Skip to content

Commit 460f145

Browse files
authored
Merge pull request #234 from lsst-dm/tickets/DM-52931/breakpoint
DM-52931 : Implement Breakpoint Node
2 parents b23ce2e + 1af4efa commit 460f145

File tree

20 files changed

+536
-51
lines changed

20 files changed

+536
-51
lines changed

packages/cm-canvas/src/App.svelte

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
import "@xyflow/svelte/dist/style.css";
33
import { SvelteFlowProvider, type Node, type Edge } from "@xyflow/svelte";
44
import CmCanvas from "./CmCanvas.svelte";
5+
import DragDropProvider from "./DragDropProvider.svelte";
56
67
type CanvasProps = {
78
nodes?: Node[];
89
edges?: Edge[];
910
[key: string]: any;
10-
}
11+
};
1112
1213
/* A set of defaultNodes for an otherwise empty canvas. Because a CM campaign
1314
always has a definite START and END, these are included. Although this canvas
@@ -42,18 +43,21 @@
4243
if the node is an editable type like "step"
4344
*/
4445
let nodes = $derived(
45-
(!incomingNodes || incomingNodes.length === 0 ? defaultNodes : incomingNodes).map(node => {
46+
(!incomingNodes || incomingNodes.length === 0
47+
? defaultNodes
48+
: incomingNodes
49+
).map((node) => {
4650
if (node.type === "step") {
4751
return {
4852
...node,
4953
data: {
5054
...node.data,
51-
handleClick: restProps.onClick
52-
}
53-
}
55+
handleClick: restProps.onClick,
56+
},
57+
};
5458
}
5559
return node;
56-
})
60+
}),
5761
);
5862
5963
let edges = $derived(
@@ -62,7 +66,9 @@
6266
</script>
6367

6468
<SvelteFlowProvider>
65-
<CmCanvas {nodes} {edges} {...restProps} />
69+
<DragDropProvider>
70+
<CmCanvas {nodes} {edges} {...restProps} />
71+
</DragDropProvider>
6672
</SvelteFlowProvider>
6773

6874
<style>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script lang="ts">
2+
import { Handle, Position, type NodeProps } from "@xyflow/svelte";
3+
4+
let { id, data }: NodeProps = $props();
5+
</script>
6+
7+
<div class="breakpoint-node">
8+
<Handle
9+
type="target"
10+
id="left"
11+
position={Position.Left}
12+
isConnectableStart={false}
13+
/>
14+
<Handle
15+
type="target"
16+
id="top"
17+
position={Position.Top}
18+
isConnectableStart={false}
19+
/>
20+
<Handle type="source" id="bottom" position={Position.Bottom} />
21+
<Handle type="source" id="right" position={Position.Right} />
22+
23+
<div class="breakpoint-node-label">
24+
<svg
25+
xmlns="http://www.w3.org/2000/svg"
26+
height="16px"
27+
viewBox="0 -960 960 960"
28+
width="16px"
29+
fill="#1f1f1f"
30+
><path
31+
d="M80-400q-33 0-56.5-23.5T0-480v-240q0-12 5-23t13-19l198-198 30 30q6 6 10 15.5t4 18.5v8l-28 128h208q17 0 28.5 11.5T480-720v50q0 6-1 11.5t-3 10.5l-90 212q-7 17-22.5 26.5T330-400H80Zm238-80 82-194v-6H134l24-108-78 76v232h238ZM744 0l-30-30q-6-6-10-15.5T700-64v-8l28-128H520q-17 0-28.5-11.5T480-240v-50q0-6 1-11.5t3-10.5l90-212q8-17 23-26.5t33-9.5h250q33 0 56.5 23.5T960-480v240q0 12-4.5 22.5T942-198L744 0ZM642-480l-82 194v6h266l-24 108 78-76v-232H642Zm-562 0v-232 232Zm800 0v232-232Z"
32+
/></svg
33+
>
34+
</div>
35+
</div>
36+
37+
<style>
38+
.breakpoint-node {
39+
background-color: var(--rubin-color-indigo-dark);
40+
border: 1px;
41+
border-color: var(--rubin-color-black-dark);
42+
border-radius: 20px;
43+
padding: 4px 4px 1px 4px;
44+
}
45+
.breakpoint-node-label {
46+
svg {
47+
fill: var(--rubin-color-white-light);
48+
}
49+
}
50+
</style>

packages/cm-canvas/src/CmCanvas.svelte

Lines changed: 165 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import StartNode from "./StartNode.svelte";
1919
import EndNode from "./EndNode.svelte";
2020
import StepNode from "./StepNode.svelte";
21+
import BreakpointNode from "./BreakpointNode.svelte";
22+
import { useDragDrop } from "./DragDropProvider.svelte";
2123
import { MarkerType, Position } from "@xyflow/system";
2224
import { onMount, tick } from "svelte";
2325
@@ -26,7 +28,7 @@
2628
edges?: Edge[];
2729
onClick?: (nodeId: string) => void;
2830
onExport?: any;
29-
}
31+
};
3032
3133
/* A mapping of custom node names to their implementation. The names used in
3234
mapping mirror the ManifestKind enum names used in the CM application.
@@ -35,24 +37,52 @@
3537
start: StartNode,
3638
end: EndNode,
3739
step: StepNode,
40+
breakpoint: BreakpointNode,
3841
} as any as NodeTypes;
3942
4043
/* the $props rune defines the inputs to the component, which can be used as
4144
arguments or attributes when the component is initialized.
4245
*/
43-
let {
44-
nodes,
45-
edges,
46-
onClick = null,
47-
onExport = null,
48-
}: CanvasProps = $props();
46+
let { nodes, edges, onClick = null, onExport = null }: CanvasProps = $props();
4947
5048
let id = $derived(nodes.length + 1);
5149
5250
// functions provided by the Svelte-Flow
5351
const { screenToFlowPosition, fitView } = useSvelteFlow();
5452
5553
const getId = () => `${id++}`;
54+
const type = useDragDrop();
55+
const onDragStart = (event: DragEvent, nodeType: string) => {
56+
if (!event.dataTransfer) {
57+
return null;
58+
}
59+
type.current = nodeType;
60+
event.dataTransfer.effectAllowed = "move";
61+
};
62+
const onDragOver = (event: DragEvent) => {
63+
event.preventDefault();
64+
if (event.dataTransfer) {
65+
event.dataTransfer.dropEffect = "move";
66+
}
67+
};
68+
const onDrop = (event: DragEvent) => {
69+
const id = getId();
70+
event.preventDefault();
71+
if (!type.current) {
72+
return;
73+
}
74+
const position = screenToFlowPosition({
75+
x: event.clientX,
76+
y: event.clientY,
77+
});
78+
const newNode = {
79+
type: type.current,
80+
id,
81+
position,
82+
data: { name: `${type.current}_${id}`, handleClick: onClick },
83+
} satisfies Node;
84+
nodes = [...nodes, newNode];
85+
};
5686
5787
const handleConnectEnd: OnConnectEnd = (event, connectionState) => {
5888
if (connectionState.isValid) return;
@@ -162,30 +192,100 @@
162192
});
163193
</script>
164194

165-
<SvelteFlow
166-
bind:nodes
167-
bind:edges
168-
{nodeTypes}
169-
{onClick}
170-
fitView
171-
onconnectend={handleConnectEnd}
172-
proOptions={{ hideAttribution: true }}
173-
nodeOrigin={[0.5, 0.5]}
174-
>
175-
<Controls />
176-
<Background variant={BackgroundVariant.Dots} />
177-
<MiniMap nodeStrokeWidth={3} />
178-
<Panel position="top-right">
179-
<button class="panel-btn" onclick={() => onLayout("TB")}
180-
>Vertical Layout</button
181-
>
182-
<button class="panel-btn" onclick={() => onLayout("LR")}
183-
>Horizontal Layout</button
184-
>
185-
</Panel>
186-
</SvelteFlow>
195+
<div class="cmcanvas">
196+
<SvelteFlow
197+
bind:nodes
198+
bind:edges
199+
{nodeTypes}
200+
{onClick}
201+
fitView
202+
onconnectend={handleConnectEnd}
203+
ondragover={onDragOver}
204+
ondrop={onDrop}
205+
proOptions={{ hideAttribution: true }}
206+
nodeOrigin={[0.5, 0.5]}
207+
>
208+
<Controls />
209+
<Background variant={BackgroundVariant.Dots} />
210+
<MiniMap nodeStrokeWidth={3} />
211+
<Panel position="top-left">
212+
<!-- svelte-ignore a11y_click_events_have_key_events -->
213+
<div class="panel-container">
214+
<div
215+
class="panel-icon clickable"
216+
role="button"
217+
tabindex="0"
218+
onclick={() => onLayout("TB")}
219+
title="Layout graph top-to-bottom"
220+
>
221+
<svg
222+
class="panel-icon"
223+
xmlns="http://www.w3.org/2000/svg"
224+
viewBox="0 -960 960 960"
225+
><path
226+
d="m480-220 160-160-56-56-64 64v-216l64 64 56-56-160-160-160 160 56 56 64-64v216l-64-64-56 56 160 160Zm0 140q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Zm0-320Z"
227+
/></svg
228+
>
229+
</div>
230+
<div
231+
class="panel-icon clickable"
232+
role="button"
233+
tabindex="0"
234+
onclick={() => onLayout("LR")}
235+
title="Layout graph left-to-right"
236+
>
237+
<svg
238+
class="panel-icon"
239+
xmlns="http://www.w3.org/2000/svg"
240+
viewBox="0 -960 960 960"
241+
><path
242+
d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q133 0 226.5-93.5T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160Zm0-320ZM380-320l56-56-64-64h216l-64 64 56 56 160-160-160-160-56 56 64 64H372l64-64-56-56-160 160 160 160Z"
243+
/></svg
244+
>
245+
</div>
246+
</div>
247+
</Panel>
248+
<Panel position="top-right">
249+
<div class="panel-container">
250+
<svg
251+
class="panel-icon"
252+
xmlns="http://www.w3.org/2000/svg"
253+
viewBox="0 -960 960 960"
254+
><path
255+
d="M440-280h80v-160h160v-80H520v-160h-80v160H280v80h160v160Zm40 200q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"
256+
/></svg
257+
>
258+
<div
259+
class="draggable panel-btn"
260+
role="button"
261+
tabindex="0"
262+
draggable={true}
263+
ondragstart={(event) => onDragStart(event, "step")}
264+
title="Drag to create a Step"
265+
>
266+
Step
267+
</div>
268+
<div
269+
class="draggable panel-btn"
270+
role="button"
271+
tabindex="0"
272+
draggable={true}
273+
ondragstart={(event) => onDragStart(event, "breakpoint")}
274+
title="Drag to create a Breakpoint"
275+
>
276+
Breakpoint
277+
</div>
278+
</div>
279+
</Panel>
280+
</SvelteFlow>
281+
</div>
187282

188283
<style>
284+
.cmcanvas {
285+
height: 100%;
286+
display: flex;
287+
flex-direction: column-reverse;
288+
}
189289
:global(.svelte-flow__handle) {
190290
opacity: 0;
191291
transition: opacity 0.2s;
@@ -218,17 +318,52 @@
218318
border: 2px solid var(--rubin-color-violet-light);
219319
}
220320
321+
.panel-container {
322+
font-family: "Source Sans Pro", "Roboto", sans-serif;
323+
display: flex;
324+
align-items: center;
325+
justify-content: center;
326+
}
327+
221328
.panel-btn {
222329
padding: 8px 16px;
223330
background: var(--rubin-color-white-light);
224331
border: 1px solid var(--rubin-color-grey-light);
225332
border-radius: 4px;
226-
cursor: pointer;
227333
font-size: 14px;
228334
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
335+
user-select: none;
336+
font-weight: 500;
337+
font-style: italic;
229338
}
230339
231340
.panel-btn:hover {
232341
background: var(--rubin-color-white-dark);
233342
}
343+
344+
.panel-icon {
345+
display: block;
346+
flex-shrink: 0;
347+
height: 32px;
348+
width: 32px;
349+
fill: var(--rubin-color-black-light);
350+
}
351+
352+
.panel-icon:hover {
353+
background: var(--rubin-color-white-dark);
354+
}
355+
356+
.draggable {
357+
cursor: grab;
358+
-webkit-user-drag: element;
359+
-webkit-transform: translateZ(0);
360+
}
361+
362+
.draggable:active {
363+
cursor: grabbing;
364+
}
365+
366+
.clickable {
367+
cursor: pointer;
368+
}
234369
</style>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!-- This context provider allows sharing draggable Nodes between the Sidebar and main Canvas
2+
using Svelte's Context API. When the drag starts, the node type is stored in context
3+
and any child component can access this via `useDragDrop`. On drop, the node type
4+
is retrieved from the context. -->
5+
6+
<script module>
7+
import { getContext } from "svelte";
8+
export const useDragDrop = () => {
9+
return getContext("dragdrop") as { current: string | null };
10+
};
11+
</script>
12+
13+
<script lang="ts">
14+
import { onDestroy, setContext, type Snippet } from "svelte";
15+
let { children }: { children: Snippet } = $props();
16+
let dragDropType = $state(null);
17+
18+
setContext("dragdrop", {
19+
set current(value) {
20+
dragDropType = value;
21+
},
22+
get current() {
23+
return dragDropType;
24+
},
25+
});
26+
27+
onDestroy(() => {
28+
dragDropType.set(null);
29+
});
30+
</script>
31+
32+
{@render children()}

0 commit comments

Comments
 (0)