Flow canvas: mark streaming edges with a marching dash
A port with `stream` set publishes repeatedly during one execution, and until now nothing on the canvas said so. `deriveEdges` carries the flag from the producing `MessageSpec` onto the edge, `LiveEdge` turns it into an `edge-stream` class, and the class draws a dashed stroke whose offset marches toward the target. The pulse still lands on top: both animations share one declaration when a value arrives on a streaming edge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
This commit is contained in:
@@ -34,7 +34,7 @@ function LiveEdgeComponent({
|
|||||||
data,
|
data,
|
||||||
selected,
|
selected,
|
||||||
}: EdgeProps) {
|
}: EdgeProps) {
|
||||||
const { message, producerId } = (data ?? {}) as FlowEdgeData
|
const { message, producerId, stream } = (data ?? {}) as FlowEdgeData
|
||||||
const live = useLiveValue(message)
|
const live = useLiveValue(message)
|
||||||
const zoom = useStore((state) => state.transform[2])
|
const zoom = useStore((state) => state.transform[2])
|
||||||
const [pulsing, setPulsing] = useState(false)
|
const [pulsing, setPulsing] = useState(false)
|
||||||
@@ -69,7 +69,7 @@ function LiveEdgeComponent({
|
|||||||
<BaseEdge
|
<BaseEdge
|
||||||
id={id}
|
id={id}
|
||||||
path={path}
|
path={path}
|
||||||
className={cn(pulsing && "edge-live")}
|
className={cn(pulsing && "edge-live", stream && "edge-stream")}
|
||||||
style={selected ? { stroke: "var(--primary)" } : undefined}
|
style={selected ? { stroke: "var(--primary)" } : undefined}
|
||||||
/>
|
/>
|
||||||
{live !== undefined && zoom >= CHIP_MIN_ZOOM ? (
|
{live !== undefined && zoom >= CHIP_MIN_ZOOM ? (
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type FlowEdgeData = {
|
|||||||
flow: string
|
flow: string
|
||||||
/** Whose publication this edge represents, so only it pulses. */
|
/** Whose publication this edge represents, so only it pulses. */
|
||||||
producerId: string
|
producerId: string
|
||||||
|
/** The producing port publishes repeatedly during one run, not once at its end. */
|
||||||
|
stream?: boolean
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,14 +42,17 @@ export type FlowEdgeData = {
|
|||||||
* message therefore draw two edges converging on the same input.
|
* message therefore draw two edges converging on the same input.
|
||||||
*/
|
*/
|
||||||
export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] {
|
export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] {
|
||||||
const producers = new Map<string, { node: string; port: string }[]>()
|
const producers = new Map<
|
||||||
|
string,
|
||||||
|
{ node: string; port: string; stream?: boolean }[]
|
||||||
|
>()
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
for (const spec of node.provides ?? []) {
|
for (const spec of node.provides ?? []) {
|
||||||
const message = qualify(flow, spec.name ?? "")
|
const message = qualify(flow, spec.name ?? "")
|
||||||
if (!message) continue
|
if (!message) continue
|
||||||
const list = producers.get(message) ?? []
|
const list = producers.get(message) ?? []
|
||||||
list.push({ node: node.id, port: portOf(spec) })
|
list.push({ node: node.id, port: portOf(spec), stream: spec.stream })
|
||||||
producers.set(message, list)
|
producers.set(message, list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +76,7 @@ export function deriveEdges(nodes: NodeDef_Input[], flow: string): Edge[] {
|
|||||||
message,
|
message,
|
||||||
flow,
|
flow,
|
||||||
producerId: `${flow}.${producer.node}`,
|
producerId: `${flow}.${producer.node}`,
|
||||||
|
stream: producer.stream,
|
||||||
} satisfies FlowEdgeData,
|
} satisfies FlowEdgeData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -90,7 +96,7 @@ export function bindingsKey(nodes: NodeDef_Input[]): string {
|
|||||||
`${node.id}|${(node.requires ?? [])
|
`${node.id}|${(node.requires ?? [])
|
||||||
.map((s) => `${portOf(s)}=${s.name ?? ""}`)
|
.map((s) => `${portOf(s)}=${s.name ?? ""}`)
|
||||||
.join(",")}|${(node.provides ?? [])
|
.join(",")}|${(node.provides ?? [])
|
||||||
.map((s) => `${portOf(s)}=${s.name ?? ""}`)
|
.map((s) => `${portOf(s)}=${s.name ?? ""}${s.stream ? "*" : ""}`)
|
||||||
.join(",")}`,
|
.join(",")}`,
|
||||||
)
|
)
|
||||||
.join(";")
|
.join(";")
|
||||||
|
|||||||
@@ -71,6 +71,32 @@
|
|||||||
stroke-width: var(--xy-edge-stroke-width);
|
stroke-width: var(--xy-edge-stroke-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* An output that publishes repeatedly during one run reads as a march rather
|
||||||
|
* than a line. Only the dash offset moves, so the pulse above still shows
|
||||||
|
* through on the same path: it recolours and widens the dashes as they go.
|
||||||
|
* A loop rather than a one-shot, so none of the `--duration-*` tokens fit —
|
||||||
|
* the literal is one dash period per step, slow enough to read as direction.
|
||||||
|
*/
|
||||||
|
.react-flow__edge-path.edge-stream {
|
||||||
|
stroke-dasharray: 6 4;
|
||||||
|
animation: edge-march 700ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Both at once: one `animation` declaration has to carry both, or the
|
||||||
|
cascade keeps whichever rule came last and drops the other. */
|
||||||
|
.react-flow__edge-path.edge-stream.edge-live {
|
||||||
|
animation:
|
||||||
|
edge-pulse var(--duration-pulse) var(--ease-emphasized),
|
||||||
|
edge-march 700ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes edge-march {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -10;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A node that just published something says so, once, and settles. */
|
/* A node that just published something says so, once, and settles. */
|
||||||
|
|||||||
Reference in New Issue
Block a user