Skip to content

Commit c16e97f

Browse files
committed
WIP launch view
1 parent 4183f4c commit c16e97f

File tree

5 files changed

+255
-113
lines changed

5 files changed

+255
-113
lines changed

src/components/launch/DependencyGraph.vue

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,25 @@ import { onMounted, ref, useTemplateRef, watch } from 'vue'
1313
1414
const props = defineProps<{ model: NodeLinkGraph }>()
1515
16+
export interface NodeLinkGraph {
17+
nodes: Record<string, GraphNodeDatum>
18+
links: GraphLinkDatum[]
19+
}
20+
1621
export interface GraphNodeDatum extends SimulationNodeDatum {
1722
id: string
1823
group: string
1924
name?: string
2025
dark?: boolean
2126
condition?: string
2227
level?: number
28+
parent?: string
2329
}
2430
2531
export interface GraphLinkDatum extends SimulationLinkDatum<GraphNodeDatum> {
2632
source: string | GraphNodeDatum
2733
target: string | GraphNodeDatum
28-
value: number
29-
}
30-
31-
export interface NodeLinkGraph {
32-
nodes: GraphNodeDatum[]
33-
links: GraphLinkDatum[]
34+
isDataLink: boolean
3435
}
3536
3637
// Component State -------------------------------------------------------------
@@ -40,6 +41,7 @@ const selectedNode = ref<GraphNodeDatum | null>(null)
4041
const zoom = ref<number>(1.0)
4142
const svgWidth = ref<number>(320)
4243
const svgHeight = ref<number>(100)
44+
const showControlLinks = ref<boolean>(true)
4345
4446
// Component Methods -----------------------------------------------------------
4547
@@ -60,24 +62,31 @@ function onSelectNode(node: GraphNodeDatum | null): void {
6062
}
6163
6264
function groupColors(group: string): string {
63-
if (group === LaunchActionType.ARG) return '#f6f294'
64-
if (group === LaunchActionType.NODE) return '#00bd7e'
65-
if (group === LaunchActionType.INCLUDE) return '#7fafe3'
65+
if (group === LaunchActionType.ARG) return 'var(--color-green)'
66+
if (group === LaunchActionType.NODE) return 'var(--color-blue)'
67+
if (group === LaunchActionType.INCLUDE) return 'var(--color-yellow)'
6668
return 'gray'
6769
}
6870
71+
function isAnyLink(): boolean {
72+
return true
73+
}
74+
75+
function isDataLink(d: GraphLinkDatum): boolean {
76+
return d.isDataLink
77+
}
78+
6979
function buildModelElement(): SVGElement {
7080
// Specify the color scale.
7181
// const color = d3.scaleOrdinal(d3.schemeCategory10)
7282
const color = groupColors
7383
74-
const nodeStrokeColor = (d: GraphNodeDatum): string => color(d.group)
75-
const nodeFillColor = (d: GraphNodeDatum): string => (d.dark ? 'var(--color-background)' : '#666')
76-
7784
// The force simulation mutates links and nodes, so create a copy
7885
// so that re-evaluating this cell produces the same result.
79-
const links = props.model.links.map((d) => ({ ...d }))
80-
const nodes = props.model.nodes.map((d) => ({ ...d }))
86+
const links = props.model.links
87+
.filter(showControlLinks.value ? isAnyLink : isDataLink)
88+
.map((d) => ({ ...d }))
89+
const nodes = Object.values(props.model.nodes).map((d) => ({ ...d }))
8190
8291
// Create a simulation with several forces.
8392
const simulation = d3
@@ -114,7 +123,7 @@ function buildModelElement(): SVGElement {
114123
.append('marker')
115124
.attr('id', 'link-arrowhead')
116125
.attr('viewBox', '0 -3 6 6')
117-
.attr('refX', 22) // about half of the link distance
126+
.attr('refX', 25) // about half of the link distance
118127
.attr('refY', 0)
119128
.attr('markerWidth', 6)
120129
.attr('markerHeight', 6)
@@ -125,16 +134,25 @@ function buildModelElement(): SVGElement {
125134
.style('stroke-opacity', 0.6)
126135
.style('fill', 'var(--color-text)')
127136
137+
const linkStrokeDasharray = (d: GraphLinkDatum): string => (d.isDataLink ? '' : '3 1')
138+
const linkMarkerEnd = (d: GraphLinkDatum): string => (d.isDataLink ? 'url(#link-arrowhead)' : '')
139+
128140
// Add a line for each link
129141
const link = svg
130142
.append('g')
131143
.attr('stroke', 'var(--color-text)')
132-
.attr('stroke-opacity', 0.6)
144+
// .attr('stroke-opacity', 0.6)
133145
.selectAll('line')
134146
.data(links)
135147
.join('line')
136-
.attr('stroke-width', (d) => Math.sqrt(d.value))
137-
.style('marker-end', 'url(#link-arrowhead)')
148+
.attr('stroke-width', 1)
149+
.attr('stroke-dasharray', linkStrokeDasharray)
150+
.style('marker-end', linkMarkerEnd)
151+
152+
const nodeStrokeColor = (d: GraphNodeDatum): string => color(d.group)
153+
const nodeFillColor = (d: GraphNodeDatum): string => (d.dark ? 'var(--color-background)' : '#666')
154+
const nodeStrokeDasharray = (d: GraphNodeDatum): string => (!d.condition ? '' : '2 1')
155+
const nodeRadius = (d: GraphNodeDatum): number => (d.level || 0) * 5 + 10
138156
139157
// Add a circle for each node
140158
const node = svg
@@ -143,9 +161,9 @@ function buildModelElement(): SVGElement {
143161
.selectAll<SVGCircleElement, GraphNodeDatum>('circle')
144162
.data(nodes)
145163
.join('circle')
146-
.attr('r', (d) => (d.level || 0) * 5 + 10)
164+
.attr('r', nodeRadius)
147165
.attr('stroke', nodeStrokeColor) // (d.dark ? '#333' : color(d.group))
148-
.attr('stroke-dasharray', (d) => (!d.condition ? '' : '2 1'))
166+
.attr('stroke-dasharray', nodeStrokeDasharray)
149167
.attr('fill', nodeFillColor)
150168
151169
node.append('title').text((d) => d.name || d.id)
@@ -269,6 +287,11 @@ function onZoomChanged(/*newValue: number*/): void {
269287
}
270288
}
271289
290+
function onShowControlLinks() {
291+
showControlLinks.value = !showControlLinks.value
292+
resetSvgElement()
293+
}
294+
272295
watch(zoom, onZoomChanged)
273296
274297
watch(() => props.model, resetSvgElement, { flush: 'post' })
@@ -283,6 +306,10 @@ onMounted(resetSvgElement)
283306
<div class="toolbar">
284307
<button @click="onZoomOut">Zoom -</button>
285308
<button @click="onZoomIn">Zoom +</button>
309+
<label>
310+
<input type="checkbox" :checked="showControlLinks" @click="onShowControlLinks" />
311+
Control Links
312+
</label>
286313
</div>
287314
<div class="panel" ref="dependencyGraphContainer">
288315
<p class="floating-label" v-if="selectedNode != null">

src/components/launch/LaunchActionList.vue

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
<script setup lang="ts">
55
// Imports ---------------------------------------------------------------------
66
7-
import type { LaunchAction, LaunchActionId } from '@/types/launch'
7+
import {
8+
LaunchActionType,
9+
type LaunchAction,
10+
type LaunchActionId,
11+
type LaunchIncludeAction,
12+
} from '@/types/launch'
813
914
// Constants -------------------------------------------------------------------
1015
@@ -34,21 +39,46 @@ function onSelectAction(action: LaunchAction): void {
3439
selected: selectedAction && action.id === selectedAction,
3540
dependency: currentDependencies.has(action.id),
3641
}"
37-
@click="onSelectAction(action)"
42+
@click.stop="onSelectAction(action)"
3843
>
39-
<span class="tag" :class="`type-${action.type}`">{{ action.type.toUpperCase() }}</span>
40-
{{ action.name }}
41-
<span v-if="currentDependencies.has(action.id)">(dep)</span>
44+
<div class="header">
45+
<span class="tag" :class="`type-${action.type}`">{{ action.type.toUpperCase() }}</span>
46+
{{ action.name }}
47+
<span v-if="currentDependencies.has(action.id)">(dep)</span>
48+
</div>
49+
50+
<div v-if="selectedAction && action.id === selectedAction" class="panel details">
51+
<i v-if="action.type === LaunchActionType.ARG">Launch file argument</i>
52+
<i v-else-if="action.type === LaunchActionType.NODE">ROS node</i>
53+
<i v-else-if="action.type === LaunchActionType.INCLUDE">Launch file inclusion</i>
54+
<!--<p v-if="action.dependencies.length > 0">
55+
Depends on:
56+
<template v-for="(dep, i) in action.dependencies" :key="dep">
57+
<template v-if="i > 0">, </template>
58+
<code>{{ dep }}</code>
59+
</template>
60+
</p>-->
61+
</div>
62+
<LaunchActionList
63+
v-if="
64+
action.type === LaunchActionType.INCLUDE &&
65+
(action as LaunchIncludeAction).actions.length > 0
66+
"
67+
:actions="(action as LaunchIncludeAction).actions"
68+
:selectedAction="selectedAction"
69+
:currentDependencies="currentDependencies"
70+
@select="onSelectAction"
71+
/>
4272
</li>
4373
</ul>
4474
</template>
4575

4676
<style>
4777
.launch-action-list {
4878
flex: 1;
49-
margin-left: 0.5rem;
50-
padding: 0 1.5rem;
51-
list-style-type: '» ';
79+
/*padding: 0 0 0 1.5rem;
80+
list-style-type: '» ';*/
81+
padding: 0 0 0 0.5rem;
5282
border-left: 1px solid var(--color-border);
5383
font-size: 0.75rem;
5484
font-family: monospace;
@@ -57,11 +87,8 @@ function onSelectAction(action: LaunchAction): void {
5787
display: flex;
5888
flex-direction: column;
5989
gap: 0.25rem;
60-
margin: 0.5rem 0 0.5rem 0.5rem;
61-
}
62-
63-
.launch-action-list > li {
64-
cursor: pointer;
90+
margin: 0.5rem 0 0 1rem;
91+
color: var(--color-text);
6592
}
6693
6794
.launch-action-list > li.selected,
@@ -71,34 +98,51 @@ function onSelectAction(action: LaunchAction): void {
7198
}
7299
73100
.launch-action-list > li.dependency {
74-
color: var(--color-orange);
101+
/*animation: text-pulse 2s infinite;*/
102+
color: var(--color-heading);
75103
}
76104
77-
.launch-action-list > li:hover {
105+
/*@keyframes text-pulse {
106+
0% {
107+
color: var(--color-text);
108+
}
109+
50% {
110+
color: var(--color-heading);
111+
}
112+
100% {
113+
color: var(--color-text);
114+
}
115+
}*/
116+
117+
.launch-action-list > li > .header {
118+
cursor: pointer;
119+
}
120+
121+
.launch-action-list > li > .header:hover {
78122
background-color: var(--color-background-mute);
79123
color: var(--color-heading);
80124
}
81125
82-
.launch-action-list > li > .tag {
126+
.launch-action-list > li > .header > .tag {
83127
border-radius: 0.25em;
84128
border: 1px solid var(--color-text);
85129
/* font-variant-caps: all-petite-caps;
86130
font-size: 1rem;*/
87131
padding: 0 0.25em;
88132
}
89133
90-
.launch-action-list > li > .tag.type-arg {
91-
border: 1px solid var(--color-yellow);
92-
color: var(--color-yellow);
93-
}
94-
95-
.launch-action-list > li > .tag.type-node {
134+
.launch-action-list > li > .header > .tag.type-arg {
96135
border: 1px solid var(--color-green);
97136
color: var(--color-green);
98137
}
99138
100-
.launch-action-list > li > .tag.type-include {
139+
.launch-action-list > li > .header > .tag.type-node {
101140
border: 1px solid var(--color-blue);
102141
color: var(--color-blue);
103142
}
143+
144+
.launch-action-list > li > .header > .tag.type-include {
145+
border: 1px solid var(--color-yellow);
146+
color: var(--color-yellow);
147+
}
104148
</style>

src/stores/launch.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import { ref } from 'vue'
77
import { defineStore } from 'pinia'
88

9-
import { LaunchActionType, type LaunchFile, type LaunchId } from '@/types/launch'
9+
import {
10+
LaunchActionType,
11+
type LaunchFile,
12+
type LaunchId,
13+
type LaunchIncludeAction,
14+
} from '@/types/launch'
1015

1116
// Constants -------------------------------------------------------------------
1217

@@ -17,19 +22,52 @@ const data: Record<LaunchId, LaunchFile> = {
1722
actions: [
1823
{ id: 'a001', type: LaunchActionType.ARG, name: 'use_sim_time', dependencies: [] },
1924
{ id: 'a002', type: LaunchActionType.ARG, name: 'simple_arg', dependencies: [] },
20-
{ id: 'a003', type: LaunchActionType.ARG, name: 'dependent_arg', dependencies: ['a002'] },
2125
{
22-
id: 'a004',
26+
id: 'a003',
2327
type: LaunchActionType.NODE,
24-
name: '/nav2_container',
25-
dependencies: ['a001', 'a003'],
28+
name: '/rviz2',
29+
dependencies: [],
2630
},
2731
{
28-
id: 'a005',
32+
id: 'a004',
2933
type: LaunchActionType.INCLUDE,
3034
name: 'nav2_bringup/launch/bringup_launch.py',
3135
dependencies: ['a001'],
32-
},
36+
actions: [
37+
{
38+
id: 'a005',
39+
type: LaunchActionType.ARG,
40+
name: 'dependent_arg',
41+
dependencies: [],
42+
},
43+
{
44+
id: 'a006',
45+
type: LaunchActionType.NODE,
46+
name: '/nav2_container',
47+
dependencies: ['a005'],
48+
},
49+
{
50+
id: 'a007',
51+
type: LaunchActionType.INCLUDE,
52+
name: 'example/launch/included_launch.py',
53+
dependencies: ['a005', 'a004'],
54+
actions: [
55+
{
56+
id: 'a008',
57+
type: LaunchActionType.ARG,
58+
name: 'dependent_arg',
59+
dependencies: [],
60+
},
61+
{
62+
id: 'a009',
63+
type: LaunchActionType.NODE,
64+
name: '/nav2_container',
65+
dependencies: ['a008'],
66+
},
67+
],
68+
} as LaunchIncludeAction,
69+
],
70+
} as LaunchIncludeAction,
3371
],
3472
},
3573
'002': {

src/types/launch.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ export interface LaunchAction {
2727
name: string
2828
dependencies: LaunchActionId[]
2929
}
30+
31+
export interface LaunchIncludeAction extends LaunchAction {
32+
type: LaunchActionType.INCLUDE
33+
actions: LaunchAction[]
34+
}

0 commit comments

Comments
 (0)