Skip to content

Links drawn only while one of their ends is selected - #171

Merged
endrix merged 17 commits into
mainfrom
feat/selection-links
Nov 12, 2026
Merged

endrix merged 17 commits into
mainfrom
feat/selection-links

Conversation

@endrix

@endrix endrix commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Fourth platform piece for the actor view of streamblocks-ide, stacked on #170 (base is that branch; retarget to main once #170 is merged). Generic: any producer can use it.

What it is for

Relations too many to draw all at once: every state variable an action reads, every action that takes a token from a port. Drawn as edges they bury the diagram; left out, the diagram cannot answer "what does this use?". Here they are drawn only for the selected node.

What changes

  • Document. An edge with meta.onSelection: true is a link, not an edge of the diagram. Its ends name nodes: a node by its id, a port by its node, a group scope by its group. meta.role becomes the class selection-link-<role> for a product to colour, meta.label a word on the link, meta.bidirectional an arrowhead at each end. A node's meta.sameAs names what it is one drawing of.
  • Server (graph-gmodel-source.ts). Such an edge is set aside: no model edge, no part in the layout, no boundary port on any frame (a pinned group it touches stays pinned). The links go to the client as data on the root (SELECTION_LINKS, SelectionLink[] in @dialogram/shared).
  • Client (selection-links.ts, drawn by the root view next to the zoom marking). For the selection: every link the selected node is an end of, the node at its other end outlined, the other drawings of the same thing outlined (dashed), the diagram's own edges faded. A link leaves a node by the side facing the other end, or by the side across when a neighbour of the node's own frame is in the way; links sharing a side are spread along it in the order of the directions they lead in, and two links between the same two nodes run side by side. The word on a link is zoom detail.

Limits

  • The links run straight (as curves) over whatever lies between their ends. Routing them around other frames is not done.
  • Links that leave a node by one side do not cross each other. Links that leave by different sides, or enter their other ends by different sides, still can: on the sample actor below, 12 pairs of 17 links cross (27 before the review's fix).
  • They are not elements: they cannot be selected, and --select in render-svg is how they are checked headlessly.

Checked

  • diagram-client/test/selection-links.test.ts (what a selection brings up, the side rule, the spreading, the drawing and where it goes) and diagram-server/test/selection-links.test.ts (set aside, ends resolved, no frame crossed, sameAs).
  • Typecheck, the neutrality gate and the whole suite pass locally.
  • Rendered with scripts/render-svg --select on a real actor with 151 links: nothing selected draws none; one action selected draws its token, state and guard links, entering the action from its sides.

After review

An independent review rendered real documents with the selection trace on and probed the transform; its findings are fixed in eac00cc:

  • Trace and links faded each other (ndf-ide turns the trace on). The trace faded the nodes the links lead to, and the links' dimming hid the trace. Now the trace leaves what the links lead to, and the other drawings of what is selected, unfaded; an edge on the trace, selected or in error is not dimmed. Checked on a render with the trace on.
  • The fan crossed itself when what it leads to is in one row seen from a side (the state variables, from an action's left side): ties fell to the link id. Now ordered by direction.
  • Ends the platform rewrites were lost. A link naming a node opened in place, or a unit's port folded into a closed bus, was dropped; a link between member ports of two closed bundles was merged into the bus edge as one of its wires. The links are now taken out before the document is rewritten and end at the frame or the bus.
  • A link between a frame and what is in it was drawn through the node; it is now dropped (a model edge of that shape is the frame's loop).
  • Cost per frame. Each side is sorted once, a node's neighbours are found once, and a model with no link is not walked on a pan or zoom frame. Measured with the review's own probe, per render:
Case Before After
30,000 elements, no links (every other product) 1.7 ms 0.0 ms
30,000 elements, 600 links, nothing selected 6.7 ms 1.3 ms
Selected node with 600 links 41 ms 13 ms
Selected node with 2,000 links 463 ms 21 ms
Selected node with 600 links in a frame of 1,000 siblings 297 ms 23 ms
  • The edges are dimmed only when a link is drawn, not for sameAs outlines alone.
  • Tests added for each of these, and one in the toolkit that a link and sameAs survive graphPayloadToDoc.

Not changed: the client tests run against a stub of the GLSP svg helper, so the vnode shapes are checked in practice by the headless render, not by a unit test.

https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

An edge whose end names a group's scope ends on the group's frame instead
of at one of its members: a transition into a state, where the state is a
group of what can happen in it. The transform resolves a scope id as an
end; `splitEdgesAtGroups` gives every such edge one place on the frame
(`GROUP_END`, left for edges ending there, right for edges starting there),
shared by all of them, crossing the frames around the group as any edge
does. The client keeps the arrowhead of an edge that ends there and draws
no dot.

An edge between a group and something inside it (or itself) goes nowhere
the frame does not already hold. It is taken out of the model and the group
is marked (`GROUP_LOOP`): one loop on the frame for all of them.

A group whose label is empty has no title and no header band; only a scope
with no label at all is named by its id. A scope's label now reaches the
document on the sidecar-export path, which dropped it.

Reopening a saved layout puts the place where edges end back where the
edge outside the group reaches it, there being no piece inside to go by.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
A producer puts the members of a group in rows with `meta.row` (a whole
number from zero, the first row on top). The members of a row sit side by
side in the order the producer gave them, the rows one under another; a
member with no row comes after the last.

The layers of a layout that runs downward are rows, and a member's row is
its partition: such a group is laid out with `elk.direction: DOWN`,
partitioning on, and its members as one component (on their own, members
with nothing between them would each be a component, packed wherever it
fits). The engine forces every nested layout to the right; a group in rows
is the exception.

The pass that pins sources and sinks to the outer layers when a diagram
opens leaves a row's members alone: a layer constraint would contradict
their row. It also no longer pins a node that has no port at all, which is
neither a source nor a sink.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
Reopening a saved layout: a port where edges end at a group has no member
to be level with, and the layout may put it below the members. The frame
was rebuilt around the members alone, so such a port was clamped to the new
bottom edge and its edges turned diagonal. The frame now reaches down past
every port its saved routes meet. A real lay out, save and reopen of such a
document is tested.

The selection trace took the port where edges end at a group for one more
port to pass through, with nothing behind it: the group was never on a
path. That port is the group's own; the group is on the path, and what it
holds is left as it is instead of faded.

A group that holds nothing was laid out 20 by 20, its title running over
its edges. Groups have a minimum size, on layout and on reopening.

The loop of a group keeps what its edges said: their captions and error
messages are its tooltip, and it is drawn in the error colour when one of
them is in error. Before, they went with the edges.

Where an edge starts at a group the dot stays (the line began 4 px off the
frame without it); only where edges arrive does the arrowhead replace it.
The property panel names the group an edge ends at by its title.

Tests: the loop from a group to a member and to itself, sharing on the way
out, group to group, an empty group's size, the trace through a group.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
A producer pins a group with `pin: 'top'` on its scope: what belongs to the
whole picture rather than to a place in its flow, as a legend does, or the
variables a state machine works on. Laid out with the rest, such a group is
one more node of the layered layout: it takes a layer, and its width moves
everything in it.

Pinned groups are taken out of the graph the layout sees, laid out apart
(their inside, and so their size), then stacked in a band above what the
layout made: the first on top, left edges on the diagram's. The rest ends
up exactly where it would without them. A pinned group that an edge crosses
or ends at stays in the flow, since its edges need it there. Routes of
edges inside a pinned group move with it.

The scope's `pin` reaches the document from a sidecar's export.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
Some relations are too many to draw all at once: every state variable an
action reads, every action that takes a token from a port. A producer marks
such an edge `meta.onSelection`. The transform sets it aside: it is no edge
of the model, takes no part in the layout and crosses no group frame (so a
pinned group it touches stays pinned). It reaches the client as data on the
root (`SELECTION_LINKS`), its ends resolved to the model's nodes: a node by
its id, a port by its node, a group scope by its group.

The client's root view draws the links of the selection over the diagram
(selection-links.ts): every link the selected node is an end of, the node
at its other end outlined, the diagram's own edges faded behind. A link
leaves a node by the side that faces the other end, or by the side across
when a neighbour of the node's own frame is in the way there, so it runs
beside a frame's rows and not over them; links sharing a side are spread
along it in the order of where they lead, so none crosses another at the
node. `meta.role` is the class `selection-link-<role>` a product colours,
`meta.label` a word on the link (hidden when zoomed out),
`meta.bidirectional` an arrowhead at each end.

A node's `meta.sameAs` names what it is one drawing of. Selecting it
outlines the other nodes that draw the same thing.

The links run straight over whatever lies between their ends; routing them
around other frames is not done.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
Rows are for members that relate only to what is outside their group. With
an edge between two members the layering puts its ends in different layers,
which split a row, and keeping the producer's order within a row sent the
edge through the members between its ends. A group with such an edge now
keeps the plain layout, laid out by its edges as before.

Which groups lay out in rows is decided once the groups are nested and the
edges cut (`arrangeGroupRows`), not while each group is built. A group
nested in a rows group is a member with a row of its own (a scope's `row`),
or after the last; a member opened in place keeps its row on the frame it
becomes. Before, both landed in the top row.

A group laid out in rows says so with `GROUP_ROWS`; `GROUP_ROW` is the row
of a member only. A row past what a layout can count is no row.

The changelog entry had been merged into the middle of its neighbour.

Left for later: a short row is not left-aligned over a long one, and a
frame grows by a track for every member's leaving edge.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
… group is not pinned

From the review of #170.

The band was placed from the root's node boxes alone. The layout runs an
edge that goes back over the top row, and the band sat on it. Its bottom
is now a gap above the highest node or route point, whichever is higher.

pin on a group nested in another was passed to the client and then
ignored by the layout. It is now dropped where the group is nested.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
…rossing, and end where a node is drawn

From the review of #171.

- The trace faded the nodes the selection's links lead to, and the links'
  dimming hid the trace. What the links lead to, and the other drawings of
  what is selected, are now left unfaded by the trace; an edge on the
  trace, selected or in error is not dimmed.
- Links sharing a side were ordered by where they lead across it, which
  ties for a row of nodes seen from a side, and then by id: the fan
  crossed itself. They are ordered by the direction they lead in; two
  links between the same two nodes run side by side.
- A link's end the platform draws as something else was lost: the links
  are taken out before the document is rewritten and end at the frame of a
  node opened in place, or at the bus a port of the unit is folded into.
  No bus counts a link among its wires. A link between a frame and what
  is in it is dropped.
- Each side is sorted once, a node's neighbours found once, and a model
  with no link and no sameAs is not walked on every frame.
- The edges are dimmed only when a link is drawn, not for outlines alone.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
@endrix
endrix changed the base branch from feat/pinned-groups to main November 12, 2026 16:01
@endrix
endrix merged commit 73000b5 into main Nov 12, 2026
1 check passed
endrix added a commit that referenced this pull request Nov 12, 2026
* feat: an edge can end at a group, and a group can loop

An edge whose end names a group's scope ends on the group's frame instead
of at one of its members: a transition into a state, where the state is a
group of what can happen in it. The transform resolves a scope id as an
end; `splitEdgesAtGroups` gives every such edge one place on the frame
(`GROUP_END`, left for edges ending there, right for edges starting there),
shared by all of them, crossing the frames around the group as any edge
does. The client keeps the arrowhead of an edge that ends there and draws
no dot.

An edge between a group and something inside it (or itself) goes nowhere
the frame does not already hold. It is taken out of the model and the group
is marked (`GROUP_LOOP`): one loop on the frame for all of them.

A group whose label is empty has no title and no header band; only a scope
with no label at all is named by its id. A scope's label now reaches the
document on the sidecar-export path, which dropped it.

Reopening a saved layout puts the place where edges end back where the
edge outside the group reaches it, there being no piece inside to go by.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: a group can be laid out in rows

A producer puts the members of a group in rows with `meta.row` (a whole
number from zero, the first row on top). The members of a row sit side by
side in the order the producer gave them, the rows one under another; a
member with no row comes after the last.

The layers of a layout that runs downward are rows, and a member's row is
its partition: such a group is laid out with `elk.direction: DOWN`,
partitioning on, and its members as one component (on their own, members
with nothing between them would each be a component, packed wherever it
fits). The engine forces every nested layout to the right; a group in rows
is the exception.

The pass that pins sources and sinks to the outer layers when a diagram
opens leaves a row's members alone: a layer constraint would contradict
their row. It also no longer pins a node that has no port at all, which is
neither a source nor a sink.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: what the review of edges ending at a group found

Reopening a saved layout: a port where edges end at a group has no member
to be level with, and the layout may put it below the members. The frame
was rebuilt around the members alone, so such a port was clamped to the new
bottom edge and its edges turned diagonal. The frame now reaches down past
every port its saved routes meet. A real lay out, save and reopen of such a
document is tested.

The selection trace took the port where edges end at a group for one more
port to pass through, with nothing behind it: the group was never on a
path. That port is the group's own; the group is on the path, and what it
holds is left as it is instead of faded.

A group that holds nothing was laid out 20 by 20, its title running over
its edges. Groups have a minimum size, on layout and on reopening.

The loop of a group keeps what its edges said: their captions and error
messages are its tooltip, and it is drawn in the error colour when one of
them is in error. Before, they went with the edges.

Where an edge starts at a group the dot stays (the line began 4 px off the
frame without it); only where edges arrive does the arrowhead replace it.
The property panel names the group an edge ends at by its title.

Tests: the loop from a group to a member and to itself, sharing on the way
out, group to group, an empty group's size, the trace through a group.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: a group can be pinned to the top

A producer pins a group with `pin: 'top'` on its scope: what belongs to the
whole picture rather than to a place in its flow, as a legend does, or the
variables a state machine works on. Laid out with the rest, such a group is
one more node of the layered layout: it takes a layer, and its width moves
everything in it.

Pinned groups are taken out of the graph the layout sees, laid out apart
(their inside, and so their size), then stacked in a band above what the
layout made: the first on top, left edges on the diagram's. The rest ends
up exactly where it would without them. A pinned group that an edge crosses
or ends at stays in the flow, since its edges need it there. Routes of
edges inside a pinned group move with it.

The scope's `pin` reaches the document from a sidecar's export.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: links drawn only while one of their ends is selected

Some relations are too many to draw all at once: every state variable an
action reads, every action that takes a token from a port. A producer marks
such an edge `meta.onSelection`. The transform sets it aside: it is no edge
of the model, takes no part in the layout and crosses no group frame (so a
pinned group it touches stays pinned). It reaches the client as data on the
root (`SELECTION_LINKS`), its ends resolved to the model's nodes: a node by
its id, a port by its node, a group scope by its group.

The client's root view draws the links of the selection over the diagram
(selection-links.ts): every link the selected node is an end of, the node
at its other end outlined, the diagram's own edges faded behind. A link
leaves a node by the side that faces the other end, or by the side across
when a neighbour of the node's own frame is in the way there, so it runs
beside a frame's rows and not over them; links sharing a side are spread
along it in the order of where they lead, so none crosses another at the
node. `meta.role` is the class `selection-link-<role>` a product colours,
`meta.label` a word on the link (hidden when zoomed out),
`meta.bidirectional` an arrowhead at each end.

A node's `meta.sameAs` names what it is one drawing of. Selecting it
outlines the other nodes that draw the same thing.

The links run straight over whatever lies between their ends; routing them
around other frames is not done.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: what the review of rows in a group found

Rows are for members that relate only to what is outside their group. With
an edge between two members the layering puts its ends in different layers,
which split a row, and keeping the producer's order within a row sent the
edge through the members between its ends. A group with such an edge now
keeps the plain layout, laid out by its edges as before.

Which groups lay out in rows is decided once the groups are nested and the
edges cut (`arrangeGroupRows`), not while each group is built. A group
nested in a rows group is a member with a row of its own (a scope's `row`),
or after the last; a member opened in place keeps its row on the frame it
becomes. Before, both landed in the top row.

A group laid out in rows says so with `GROUP_ROWS`; `GROUP_ROW` is the row
of a member only. A row past what a layout can count is no row.

The changelog entry had been merged into the middle of its neighbour.

Left for later: a short row is not left-aligned over a long one, and a
frame grows by a track for every member's leaving edge.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: lines of text in a node, read close in

A producer puts lines of text in a node with meta.lines: what a reader
zooms in for, such as the condition a step waits on or what it takes and
gives. Each line is a band across the bottom of the node's body, which is
taller by them and as wide as the longest. The band is always drawn and
carries node-line-<kind> for a product to colour; its text is detail,
drawn only close in.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: the pinned band clears the diagram's highest route, and a nested group is not pinned

From the review of #170.

The band was placed from the root's node boxes alone. The layout runs an
edge that goes back over the top row, and the band sat on it. Its bottom
is now a gap above the highest node or route point, whichever is higher.

pin on a group nested in another was passed to the client and then
ignored by the layout. It is now dropped where the group is nested.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: links and the selection's trace read together, fan out without crossing, and end where a node is drawn

From the review of #171.

- The trace faded the nodes the selection's links lead to, and the links'
  dimming hid the trace. What the links lead to, and the other drawings of
  what is selected, are now left unfaded by the trace; an edge on the
  trace, selected or in error is not dimmed.
- Links sharing a side were ordered by where they lead across it, which
  ties for a row of nodes seen from a side, and then by id: the fan
  crossed itself. They are ordered by the direction they lead in; two
  links between the same two nodes run side by side.
- A link's end the platform draws as something else was lost: the links
  are taken out before the document is rewritten and end at the frame of a
  node opened in place, or at the bus a port of the unit is folded into.
  No bus counts a link among its wires. A link between a frame and what
  is in it is dropped.
- Each side is sorted once, a node's neighbours found once, and a model
  with no link and no sameAs is not walked on every frame.
- The edges are dimmed only when a link is drawn, not for outlines alone.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* docs: the pinned groups entry as main has it

The merge of main left the entry's earlier wording beside the new one.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: a line of text is cut by character, takes no control character, and rows stand on their tops

From the review of #172.

- The cut at 48 fell between the two halves of a character outside the
  basic plane, and the headless render died on the lone half. The cut is
  now by character and by column: a wide character (East Asian, an emoji)
  takes two, in the estimate of the node's width as well.
- Control characters reached the exported SVG raw and made it no XML;
  they, the marks that turn the direction of the text, and the spaces of
  no width are taken out of a line.
- An if or a loop took lines on the server, which its own view never
  drew: it takes none.
- The members of a row of a group stand on one line along their tops: a
  node with lines beside one without was centred on it.
- The parser sits with the other meta parsers.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
endrix added a commit that referenced this pull request Nov 12, 2026
* feat: an edge can end at a group, and a group can loop

An edge whose end names a group's scope ends on the group's frame instead
of at one of its members: a transition into a state, where the state is a
group of what can happen in it. The transform resolves a scope id as an
end; `splitEdgesAtGroups` gives every such edge one place on the frame
(`GROUP_END`, left for edges ending there, right for edges starting there),
shared by all of them, crossing the frames around the group as any edge
does. The client keeps the arrowhead of an edge that ends there and draws
no dot.

An edge between a group and something inside it (or itself) goes nowhere
the frame does not already hold. It is taken out of the model and the group
is marked (`GROUP_LOOP`): one loop on the frame for all of them.

A group whose label is empty has no title and no header band; only a scope
with no label at all is named by its id. A scope's label now reaches the
document on the sidecar-export path, which dropped it.

Reopening a saved layout puts the place where edges end back where the
edge outside the group reaches it, there being no piece inside to go by.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: a group can be laid out in rows

A producer puts the members of a group in rows with `meta.row` (a whole
number from zero, the first row on top). The members of a row sit side by
side in the order the producer gave them, the rows one under another; a
member with no row comes after the last.

The layers of a layout that runs downward are rows, and a member's row is
its partition: such a group is laid out with `elk.direction: DOWN`,
partitioning on, and its members as one component (on their own, members
with nothing between them would each be a component, packed wherever it
fits). The engine forces every nested layout to the right; a group in rows
is the exception.

The pass that pins sources and sinks to the outer layers when a diagram
opens leaves a row's members alone: a layer constraint would contradict
their row. It also no longer pins a node that has no port at all, which is
neither a source nor a sink.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: what the review of edges ending at a group found

Reopening a saved layout: a port where edges end at a group has no member
to be level with, and the layout may put it below the members. The frame
was rebuilt around the members alone, so such a port was clamped to the new
bottom edge and its edges turned diagonal. The frame now reaches down past
every port its saved routes meet. A real lay out, save and reopen of such a
document is tested.

The selection trace took the port where edges end at a group for one more
port to pass through, with nothing behind it: the group was never on a
path. That port is the group's own; the group is on the path, and what it
holds is left as it is instead of faded.

A group that holds nothing was laid out 20 by 20, its title running over
its edges. Groups have a minimum size, on layout and on reopening.

The loop of a group keeps what its edges said: their captions and error
messages are its tooltip, and it is drawn in the error colour when one of
them is in error. Before, they went with the edges.

Where an edge starts at a group the dot stays (the line began 4 px off the
frame without it); only where edges arrive does the arrowhead replace it.
The property panel names the group an edge ends at by its title.

Tests: the loop from a group to a member and to itself, sharing on the way
out, group to group, an empty group's size, the trace through a group.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: a group can be pinned to the top

A producer pins a group with `pin: 'top'` on its scope: what belongs to the
whole picture rather than to a place in its flow, as a legend does, or the
variables a state machine works on. Laid out with the rest, such a group is
one more node of the layered layout: it takes a layer, and its width moves
everything in it.

Pinned groups are taken out of the graph the layout sees, laid out apart
(their inside, and so their size), then stacked in a band above what the
layout made: the first on top, left edges on the diagram's. The rest ends
up exactly where it would without them. A pinned group that an edge crosses
or ends at stays in the flow, since its edges need it there. Routes of
edges inside a pinned group move with it.

The scope's `pin` reaches the document from a sidecar's export.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: links drawn only while one of their ends is selected

Some relations are too many to draw all at once: every state variable an
action reads, every action that takes a token from a port. A producer marks
such an edge `meta.onSelection`. The transform sets it aside: it is no edge
of the model, takes no part in the layout and crosses no group frame (so a
pinned group it touches stays pinned). It reaches the client as data on the
root (`SELECTION_LINKS`), its ends resolved to the model's nodes: a node by
its id, a port by its node, a group scope by its group.

The client's root view draws the links of the selection over the diagram
(selection-links.ts): every link the selected node is an end of, the node
at its other end outlined, the diagram's own edges faded behind. A link
leaves a node by the side that faces the other end, or by the side across
when a neighbour of the node's own frame is in the way there, so it runs
beside a frame's rows and not over them; links sharing a side are spread
along it in the order of where they lead, so none crosses another at the
node. `meta.role` is the class `selection-link-<role>` a product colours,
`meta.label` a word on the link (hidden when zoomed out),
`meta.bidirectional` an arrowhead at each end.

A node's `meta.sameAs` names what it is one drawing of. Selecting it
outlines the other nodes that draw the same thing.

The links run straight over whatever lies between their ends; routing them
around other frames is not done.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: what the review of rows in a group found

Rows are for members that relate only to what is outside their group. With
an edge between two members the layering puts its ends in different layers,
which split a row, and keeping the producer's order within a row sent the
edge through the members between its ends. A group with such an edge now
keeps the plain layout, laid out by its edges as before.

Which groups lay out in rows is decided once the groups are nested and the
edges cut (`arrangeGroupRows`), not while each group is built. A group
nested in a rows group is a member with a row of its own (a scope's `row`),
or after the last; a member opened in place keeps its row on the frame it
becomes. Before, both landed in the top row.

A group laid out in rows says so with `GROUP_ROWS`; `GROUP_ROW` is the row
of a member only. A row past what a layout can count is no row.

The changelog entry had been merged into the middle of its neighbour.

Left for later: a short row is not left-aligned over a long one, and a
frame grows by a track for every member's leaving edge.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: lines of text in a node, read close in

A producer puts lines of text in a node with meta.lines: what a reader
zooms in for, such as the condition a step waits on or what it takes and
gives. Each line is a band across the bottom of the node's body, which is
taller by them and as wide as the longest. The band is always drawn and
carries node-line-<kind> for a product to colour; its text is detail,
drawn only close in.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: a group can be marked as where the flow starts

A producer marks a group with start: true on its scope: the initial
state of a state machine, the first stage of a pipeline. Its title gets
a filled dot before it, as a statechart marks its initial state. A group
with no title has no band for it and draws none.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: the pinned band clears the diagram's highest route, and a nested group is not pinned

From the review of #170.

The band was placed from the root's node boxes alone. The layout runs an
edge that goes back over the top row, and the band sat on it. Its bottom
is now a gap above the highest node or route point, whichever is higher.

pin on a group nested in another was passed to the client and then
ignored by the layout. It is now dropped where the group is nested.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: links and the selection's trace read together, fan out without crossing, and end where a node is drawn

From the review of #171.

- The trace faded the nodes the selection's links lead to, and the links'
  dimming hid the trace. What the links lead to, and the other drawings of
  what is selected, are now left unfaded by the trace; an edge on the
  trace, selected or in error is not dimmed.
- Links sharing a side were ordered by where they lead across it, which
  ties for a row of nodes seen from a side, and then by id: the fan
  crossed itself. They are ordered by the direction they lead in; two
  links between the same two nodes run side by side.
- A link's end the platform draws as something else was lost: the links
  are taken out before the document is rewritten and end at the frame of a
  node opened in place, or at the bus a port of the unit is folded into.
  No bus counts a link among its wires. A link between a frame and what
  is in it is dropped.
- Each side is sorted once, a node's neighbours found once, and a model
  with no link and no sameAs is not walked on every frame.
- The edges are dimmed only when a link is drawn, not for outlines alone.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* docs: the pinned groups entry as main has it

The merge of main left the entry's earlier wording beside the new one.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* fix: a line of text is cut by character, takes no control character, and rows stand on their tops

From the review of #172.

- The cut at 48 fell between the two halves of a character outside the
  basic plane, and the headless render died on the lone half. The cut is
  now by character and by column: a wide character (East Asian, an emoji)
  takes two, in the estimate of the node's width as well.
- Control characters reached the exported SVG raw and made it no XML;
  they, the marks that turn the direction of the text, and the spaces of
  no width are taken out of a line.
- An if or a loop took lines on the server, which its own view never
  drew: it takes none.
- The members of a row of a group stand on one line along their tops: a
  node with lines beside one without was centred on it.
- The parser sits with the other meta parsers.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv

* feat: the group a flow starts in is marked initial, with a dot and an arrow, title or no title

From the review of #173.

- The scope's field is initial and the argument GROUP_INITIAL: start read
  as the pair of GROUP_END, which is where an edge meets a frame, and as
  a place the layout would put the group in. It does neither.
- The mark is a dot and a short arrow, as a statechart draws it: a dot
  alone was the glyph of a port.
- A group with no title has the mark alone, in the room every frame keeps
  for a header; it was dropped without a word.
- The test renders the group's view and checks what is drawn and where,
  where it read the view's source.

Claude-Session: https://claude.ai/code/session_01EToj6RJRMSSHQiqGNMU9wv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant