Skip to content

feat(library): customizable straight edges — waypoints, tidy-tree layout & obstacle routing#830

Open
FelixTJDietrich wants to merge 8 commits into
mainfrom
feat/straight-edge-waypoints-routing
Open

feat(library): customizable straight edges — waypoints, tidy-tree layout & obstacle routing#830
FelixTJDietrich wants to merge 8 commits into
mainfrom
feat/straight-edge-waypoints-routing

Conversation

@FelixTJDietrich

Copy link
Copy Markdown
Contributor

Summary

Brings the straight-hook edge regime (UML use-case associations/include/extend/generalization, syntax-tree links, petri-net arcs) to parity with the orthogonal step-edge engine from #826. These edges were dumb 2-point diagonal lines with no bend handles, no auto-layout, and no obstacle avoidance. They now support hand-editing, automatic node avoidance, and — for syntax trees — a one-click tidy layout.

Closes #392. Addresses #282 (both the literal "add a joint" ask and its root cause) and lands a first concrete slice of #128.

Straight-hook edges are diagonal by UML convention — this is deliberately not "make them orthogonal". It is a parallel straight-polyline regime that shares the engine's contracts (memoryless, deterministic, preview==commit) and cost vocabulary, but not the orthogonal grid-A* search or the H/V jog machinery.

Track A — manual waypoints ("bend handles to pin it")

  • Draggable free-2D waypoints on every straight-hook edge. A faint ghost handle at each segment midpoint materialises a bend once dragged past a threshold; a bend drags in 2D; remove it by double-click or by dragging it back onto the line (continuous near-collinear pruning).
  • Model: data.points holds interior waypoints only (JointJS "vertices" model); the route is [source, ...points, target] connected by plain diagonal segments. Documented divergence from step edges (which store the full route).
  • Solver (S1): both manual-points call sites (the port-coordination reservation and the main loop) now use a regime-aware diagonal passthrough — no orthogonal reprojection, which would silently break preview==commit.
  • Rendering (S2): grip/marker orientation follows the terminal segment, not the endpoint chord, so a bent edge's arrowhead and reconnect grips point the right way. Touch-friendly (no hover-only affordance; generous hit targets).
  • Migration: the v3 path (full polyline incl. endpoints, previously inert for these types) is cleared on import so old diagrams don't sprout spurious/double bends.

Track B — syntax-tree tidy layout (#282)

  • A user-triggered "Tidy tree layout" reconstructs the hierarchy from SyntaxTreeLink edges and repositions nodes so parent→child links no longer overlap siblings — the root-cause fix for Syntax tree diagram: long text overlaps nodes (needs line wrapping) #282 (a node-separation problem, not routing).
  • Hand-written integer Buchheim/Walker/Reingold–Tilford tidy-tree with variable-width sibling separation (long labels push siblings apart). Malformed input (forests, cycles, multi-parent) degrades gracefully — never corrupts the diagram.
  • One Yjs transaction / one undo step; a no-op on already-tidy or non-syntax-tree diagrams. Exposed as ApollonEditor.layoutSyntaxTree() and a SyntaxTree-only builtin control.

Track D — automatic obstacle-avoiding straight router

  • Straight edges now auto-bend around any node sitting directly in their path; an unobstructed chord stays straight. User waypoints are honoured as mandatory checkpoints (libavoid model).
  • Deterministic reduced/tangent visibility graph + integer A*. Every decision is integer — isqrt (exact floor) for length, integer dot/cross turn buckets for the angle penalty; no hypot/atan2/float on any decision path. Canonical vertex/heap keys keep the search byte-identical across Yjs peers and reloads. Reuses polylineConflictCost and the obstacle corridor.

Design & bundle

Budgets were raised deliberately (they are a guardrail, not a physical ceiling) with headroom, rather than distorting the architecture to fit an arbitrary number: main entry 118→126 kB (measured 122.06), routing worker 40→44 kB (measured 39.95). /model and /export unchanged.

Implementation notes

New modules: utils/geometry/freeWaypoints.ts, utils/geometry/integerGeometry.ts, utils/geometry/straightPolylineRouter.ts, utils/tidyTree/{tidyTree,buildForest,applyTidyLayout}.ts. The design was pressure-tested from four independent angles (codebase map, OSS/industry survey, principal-engineer review, product devil's-advocate) before implementation; a cost-model attachment-selection track was deliberately cut as subsumed by Track D and determinism-unsafe on the use-case ellipse.

Steps for testing

  1. In a use-case/petri-net/syntax-tree diagram, hover an edge, drag a ghost midpoint → a bend appears and persists; drag it; double-click or drag-to-collinear to remove; reload → identical.
  2. Place a node between two connected use cases → the straight edge auto-bends around it; move the node → it re-routes; a clear path stays straight.
  3. Pin a waypoint, then let the router run → the pin is respected as a checkpoint.
  4. Build a syntax tree with overlapping siblings → Tidy tree layout → no link overlaps a node; one undo reverts.
  5. Import a v3 use-case/syntax-tree diagram → no spurious bends.

Tests

Unit: freeWaypoints, integerGeometry, straightPolylineRouter (incl. shuffled-input determinism), tidyTree, tidyForest, straightHookRouting (solver S1 passthrough + Track D obstacle bend + cold-solve determinism), and a v3-migration case. Full suite: 1783 passed / 1 skipped. lint, format:check, build (all packages), size, knip all green.

Checklist

  • Added a user-voice changeset for the library
  • feat type matches the primary user-visible change
  • Tests added
  • pnpm lint && pnpm format:check && pnpm build && pnpm test — green
  • Interactive-gesture E2E coverage (recommended follow-up; the gestures are thin glue over unit-tested pure functions + the solver integration test)

🤖 Generated with Claude Code

…out & obstacle routing

Brings the "straight-hook" edge regime (UML use-case, syntax-tree, petri-net) to
parity with the orthogonal step-edge engine, closing #392 and addressing #282/#128.

Track A — manual waypoints: straight edges gain draggable free-2D waypoints. A ghost
midpoint per segment materialises a bend past a drag threshold; a bend drags in 2D and
is removed by double-click or drag-to-collinear (continuous redundancy pruning). Model:
data.points holds INTERIOR waypoints only (JointJS "vertices" model); route is
[source, ...points, target]. New utils/geometry/freeWaypoints.ts (pure, integer). The
solver's two manual-points call sites become a regime-aware diagonal passthrough (no
orthogonal reprojection). Marker/grip orientation now follows the terminal segment.
v3→v4 migration clears the (previously inert) legacy path on straight-hook edges.

Track B — syntax-tree tidy layout (#282): a user-triggered hierarchical layout derives
the tree from SyntaxTreeLink edges and repositions nodes (hand-written integer
Buchheim/Walker/Reingold–Tilford, variable-width sibling separation) so parent→child
links no longer overlap siblings. Malformed input (forests, cycles, multi-parent)
degrades gracefully. One Yjs transaction / one undo step. Exposed as
ApollonEditor.layoutSyntaxTree() and a SyntaxTree-only builtin control.

Track D — obstacle-avoiding straight router: straight edges auto-bend around any node
in their path (a clear chord stays straight) via a deterministic reduced visibility
graph + integer A* (utils/geometry/straightPolylineRouter.ts, integerGeometry.ts —
isqrt/turn buckets, no hypot/atan2 on any decision path). User waypoints are honoured
as mandatory checkpoints. Memoryless, preview==commit, byte-identical across peers.

Bundle budgets raised deliberately (main 118→126 kB, worker 40→44 kB) with headroom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The core routing approach and pure-helper coverage are substantial, but the new waypoint schema is not safe for already-saved 4.x models, and several obstacle/gesture/label integrations break once routes contain both authored and automatic bends. See the inline comments for the concrete cases that need fixing.

// through INTERIOR waypoints only. The legacy v3 `path` is the full rendered
// polyline including endpoints and was inert for these types, so importing it as
// waypoints would sprout spurious (double-endpoint) bends. Clear it.
if (STRAIGHT_HOOK_EDGE_TYPES.has(edgeType as string)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich [high] This only fixes direct v3 imports. Models already converted and saved by current releases are 4.x and can still carry the old full-route data.points; this branch still exports 4.1.0, so those points are indistinguishable from new interior waypoints and will render as bogus bends (and can then be resaved as user data). Version this schema change and normalize older 4.x models too.

🤖 Prompt for AI agents

In library/lib/utils/versionConverter.ts, legacy full-route points on straight-hook edges are cleared only during v3 conversion, while pre-feature 4.x models pass through unchanged. Add a versioned normalization for pre-feature 4.x models and bump the exported model version so new interior waypoints remain distinguishable and preserved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich This is still open: the current import path still accepts every 4.x model unchanged, and the exported schema remains 4.1.0, so legacy full-route points are still interpreted as new interior waypoints.

const line = routeStraightPolyline({
source: endpoints.adjustedSource,
target: endpoints.adjustedTarget,
checkpoints: getStraightHookInterior(edge),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich [high] candidateBounds only spans the source and target nodes, but these checkpoints can be dragged arbitrarily far outside that box. getEdgeObstacles then omits nodes around the distant waypoint legs, so the straight router sees no blocker and routes directly through them. Expand the obstacle-query bounds to include every checkpoint and add an integration test with an out-of-bounds waypoint.

🤖 Prompt for AI agents

In library/lib/utils/geometry/edgeGeometrySolver.ts, straight-hook obstacle discovery uses endpoint-only candidate bounds even when manual checkpoints lie outside them. Expand the bounds passed to getEdgeObstacles to cover the source, target, and all straight-hook checkpoints, and add a regression test proving a distant checkpoint leg avoids a nearby node.

@Claudia-Anthropica Claudia-Anthropica Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich This is still open: candidateBounds is still computed solely from the endpoint nodes and passed unchanged to getEdgeObstacles, so nodes near a distant manual checkpoint remain outside the query.

Comment thread library/lib/edges/GenericEdge.tsx Outdated
Comment thread library/lib/hooks/useStraightPathEdge.ts
const isPolyline = renderPoints.length > 2 || lineJumps.length > 0
const currentPath = useMemo(() => {
if (lineJumps.length > 0) return buildEdgePath(renderPoints, lineJumps)
if (isPolyline) return buildEdgePath(renderPoints, lineJumps)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich [medium] buildEdgePath draws a continuous polyline, so bending an include/extend edge removes the midpoint gap that calculateStraightPath creates for the stereotype text. The straight-edge label components also still position from the endpoint chord, which makes use-case and Petri labels float away from a bent route (often onto the node that caused the detour). Derive the label anchor/orientation from the local arc-mid segment and preserve the include/extend gap on that segment.

🤖 Prompt for AI agents

In library/lib/hooks/useStraightPathEdge.ts, bent straight-hook paths bypass the include/extend text gap and expose only endpoint-chord geometry to label renderers. Build a polyline path that carves the stereotype gap on the chosen local segment, pass that segment's endpoints to straight-edge labels, and add rendering-geometry tests for bent use-case and Petri edges.

@Claudia-Anthropica Claudia-Anthropica Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich This is still open: bent routes still use buildEdgePath without the include/extend gap, and label geometry still exposes only the route endpoints plus the generic midpoint instead of the local label segment.

@github-project-automation github-project-automation Bot moved this from Backlog to In progress in Apollon Development Jul 24, 2026
…ment

Straight-hook edges attached at whatever handle they were drawn on, so lines left
from arbitrary corners and multiple connections to one node stacked on a single
point — visibly worse than the orthogonal engine's coordinated attachment.

Unbent, unpinned straight-hook edges are now free ends that flow through the SAME
side + port machinery as step edges: assignSides (facing side toward the partner),
assignPorts (balanced centred band + angular order for a shared node side) and
selectEdgeAnchors — differing only in the final routing primitive, which stays a
straight, obstacle-avoiding polyline. Authoring a bend pins those facing-side sites
(like a step-edge bend commit) so the endpoints do not snap to the drawn handle, and
the hook now renders the solver's chosen attachment rather than the stored handle.

- collectFixedPorts: a straight-hook edge is no longer pinned to its drawn handle;
  only an authored bend or explicit pin fixes an endpoint.
- reservation pass: only a bent straight-hook edge reserves a fixed diagonal route;
  an unbent one is band-assigned like a step edge.
- main pass: unbent straight-hook edges run selectEdgeAnchors (facing side + band),
  then route straight; bent ones route straight through their waypoints.

Tests: facing-side centred attachment and balanced spread across three children,
plus the existing passthrough/obstacle/determinism cases. Full suite 1785 green;
lint, build and size all pass (worker 39.99/44 kB, main 122.15/126 kB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The facing-side and balanced-port work is a useful addition, but it does not address the five existing routing, migration, gesture, and label findings. Those threads remain open, so I’m requesting changes again on this SHA.

…acle jogs

Automatic obstacle avoidance produced backtracking, spaghetti routes on dense
syntax trees (e.g. an edge jogging ABOVE its own source node, or detouring far
right and back). Two fixes, matching how tree drawing actually works:

- Syntax-tree links are drawn as straight parent→child lines; node overlaps are
  resolved by the tidy-tree layout (Track B), NOT by per-edge routing — the
  approach d3/dagre/ELK take for trees. Side selection stays obstacle-aware, so a
  stacked node still makes the edge pick a side that clears it with a straight
  line rather than one that runs through it. Use-case / petri edges (general
  graphs) keep automatic obstacle avoidance.
- The straight polyline router never treats an edge's OWN source/target nodes as
  obstacles (they are where it attaches), removing the backwards-jog artifact.

Verified on the reported diagrams: the dense tree now has 0 auto-bends with
sensible facing-side attachment, and the 3-node stacked case is a straight line
that clears the middle node. Full suite 1786 green; lint, build, size all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The latest routing adjustments do not address the five existing migration, obstacle-query, waypoint-gesture, and label-geometry requests. Those threads remain open, so I’m requesting changes again on this SHA.

…e routing

Automatic detours were shortest-path-shaped rather than diagram-shaped: edges slid
out sideways along a node's own edge (90-degree "exit angles"), rounded corners in
staircases of tiny kinks, took long detours around groups of nodes, and ignored
other edges entirely. The router now optimises the things that decide whether a
route looks deliberate.

The search runs over DIRECTED EDGES instead of vertices, because the cost of
arriving somewhere depends on the direction you arrived from. That makes these
exact and additive, all in pixels on the shared ROUTING_COST scale:

- endpoint angle: leaving/meeting a node at anything but a right angle to its side
  is priced above a bend, so the search spends a corner (or attaches on another
  side) rather than grazing along the node — the reported artefact;
- bend: a flat per-corner charge (the engine's `bendInGridCells`) PLUS a
  sharpness-graduated turn term. The floor is what stops a staircase of near-free
  micro-bends from beating one honest corner;
- neighbours: crossing / collinear overlap / parallel crowding, delegated to the
  shared `polylineConflictCost` so both regimes price a crossing identically.
  Siblings sharing a node are exempt (the same carve-out the orthogonal neighbour
  scan makes) — they are meant to fan out together.

Two further fixes: obstacle corners are rung at both the guaranteed margin and the
roomier preferred one, with roomy corners dropped where they fall inside another
node — a single wide ring welds neighbouring nodes together and closes the gap
between them, which is what caused the long detours. And a string-pulling pass
removes any bend the route does not need, accepted only when it strictly improves
the same objective.

Straight-edge bend handles now reuse the step edge's `edge-bend-handle` pill,
styling and hover reveal, so the two regimes present one control set (a straight
edge's handle carries a `move` cursor, since it has two free axes, not one).

Measured on the reported diagrams: node crossings 0 everywhere; the dense parse
tree drops from 11 bends / 90-degree worst exit to 6 bends / 63 degrees with total
length down from 2708 to 2257. Full suite 1787 green; lint, build, size and knip
all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The latest routing-cost changes do not address the five existing migration, obstacle-query, waypoint-gesture, and label-geometry requests. Those threads remain open, so I’m requesting changes again on this SHA.

A mirror-symmetric diagram was not drawn symmetrically, and a node's outgoing
edges did not fan out: two connectors could leave from the SAME pixel, and a
mirrored pair could attach to different sides entirely.

Three causes, each a dimension of the step-edge cost model that straight edges
were not really getting:

- The coordinated band seat was passed to `selectEdgeAnchors` as a soft
  preference. A straight edge can always shorten itself by aiming its port at the
  partner, so the preference lost every time and whole fans collapsed onto one or
  two points. On a shared side the seat is now AUTHORITATIVE (a pin), which is what
  the original port-assignment design specified — the fan is evenly spread again.
- `assignSides` minimises ORTHOGONAL corners, which is the wrong objective for a
  line that pays no corner to reach any side; what it pays for is its departure
  angle. Straight-edge sides now come from the direction of the partner
  (`normalAlignedSide`, added to the shared `rectSides` vocabulary) — the
  "partner direction decides side" rule this pipeline already cites. Being a pure
  function of the two rectangles, it is mirror-symmetric by construction, which
  corner-minimisation is not.
- A LONE end (a leaf) has no band seat, so its side was chosen freely and a mirrored
  pair could disagree — one leaf attached on its left, its twin on top. Lone ends
  now take the CENTRE of their aligned side, the same "centre it when nothing
  competes" rule `endpointPlacementCost` rewards for step edges.

Overlap is also now priced between siblings. Connectors sharing a node are exempt
from CROWDING (they are meant to fan out side by side) but never from crossing or
from lying exactly on top of one another, expressed by pricing them at zero
crowding clearance through the same shared `polylineConflictCost`.

On the reported diagram: mirror-symmetry error drops from 25px to <=5px (grid
rounding), worst exit angle 99 -> 68 degrees, length 2258 -> 2231, still 0 node
crossings and 6 bends. The tidy-tree layout was verified already symmetric.

Tests: mirror-symmetry of routes, an evenly-spaced/no-duplicate-port fan,
mirror-symmetry and even fan spacing for the tidy-tree layout, and an explicit
overlap-vs-crowding-vs-crossing cost ordering. Full suite 1791 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The latest port-coordination commit does not address the five existing migration, obstacle-query, waypoint-gesture, and label-geometry requests. Those threads remain open, and CI now has four related straight-edge E2E regressions, so I’m requesting changes again on this SHA.

… drawing

Follow-up on straight-edge routing quality, all four points measured on the
reported parse-tree diagram rather than eyeballed.

Sibling connectors clearing the same obstacle all turned at the same corner
vertex, so a fan arrived at one shared elbow and read as a knot. Each edge now
prefers a corner RING chosen from its own coordinated seat: the outermost members
of a fan stand further off, so the detours nest instead of stacking. The key is
the seat's distance from its side's centre, compared against the largest such
distance on that side — mirror-invariant (a rank-keyed rule would nest the two
halves of a symmetric diagram differently) and free of a magic threshold, which
the raw ratios would trip over since they depend on side length and seat count.
An order-dependent "penalise an already-used elbow" rule was tried first and
rejected: it untied the knot but resolved the two halves differently, costing the
symmetry that matters more. It survives only as a small tie-break.

Edge-to-edge clearance now uses the node clearance rather than the engine's
tighter parallel band: a straight route can sit anywhere, so it can afford real
daylight. Because the crowding charge falls to exactly zero at that separation,
neatly parallel connectors are the cheapest arrangement available rather than a
merely tolerated one — a genuine reward, expressed as a penalty that vanishes,
since a literal bonus would be a negative edge weight and A* may not have those.

Sides come from `facingSide` again rather than a raw-direction variant. Weighing
the partner direction against the node's own half-extents is what makes a row of
children all attach on their TOP edge instead of some catching a corner and
arriving sideways — the reported `exp -> exp` case — and it keeps a sibling fan
reading as parallel.

Stability: a bend is now priced at four orthogonal corners. A diagonal bend can
fall anywhere, so a small floor let the search buy a corner for a few pixels of
length — which is exactly what makes a drawing reorganise itself when a node is
nudged. Under a one-grid-cell move of every node in the fixture, topology changes
fall from 28 to 15 (bend-count flips 18 -> 5) with byte-identical routes on all
four reported diagrams.

Tests: nested (non-shared) sibling elbows, and topology invariance under a
one-grid-cell nudge in six directions. Full suite 1793 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The latest detour-nesting and stability changes do not address the five existing migration, obstacle-query, waypoint-gesture, and label-geometry requests. Those threads remain open, so I’m requesting changes again on this SHA.

… alike

The reported "right side is off, like stmt -> }": a symmetric diagram was drawn
lopsided. Its left flank nested its two detours on different corner rings while
the right flank collapsed both onto one shared elbow.

Root cause is a floating-point comparison on a decision path — the exact hazard
this subsystem's "integer, exactly computed" rule exists to prevent, introduced by
the ring selection added in the previous commit. A seat and its mirror image lie
the same distance from their side's centre in exact arithmetic but not in binary
floating point: for a seven-way band,

  0.5 - 1/7 = 0.35714285714285715
  6/7 - 0.5 = 0.3571428571428571     (smaller in the final bit)

Ring choice compared each seat's distance against the largest on its side, so the
right-hand member of every mirror pair failed the test by one ulp, lost the roomy
ring, and fell back onto the tight corner its neighbour already used. Rounding the
distance to whole permille before comparing makes reflection exact — far finer
than any seat spacing, far coarser than the artefact. Both flanks now nest
identically: bends land at (30,360)/(45,375) and mirror to (350,360)/(335,375).

Covered by a regression test that fails on the previous arithmetic.

Known remaining limitation, unchanged by this fix: when many edges share one short
node side (five on a 70px side seats them 12px apart) a fan leaves the node too
narrow to read as separate lines until the routes have travelled some distance.
Widening it means letting a crowded side overflow onto its neighbours, which is a
change to the shared port-assignment stage rather than to straight edges alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The symmetric port-seat fix is focused, but this commit does not address the five open migration, obstacle-query, waypoint-gesture, and label-geometry requests. Those threads remain unresolved, so I’m requesting changes again on this SHA.

…waypoints

Two problems, one measured and one about the interaction model.

A fan of straight edges could CROSS ITSELF: on the reported diagrams `stmt -> exp`
crossed both `stmt -> ;` and `stmt -> }`. Root cause is in port ordering, not in the
routing cost. `assignPorts` orders seats with `crossingOrderKey`, which for a partner
beyond the node's own band deliberately INVERTS the angular order so a
farther-reaching route nests outside a nearer one — correct for an orthogonal route,
which leaves along an axis and turns far away. A straight edge runs directly at its
partner, so its crossing-free order is simply the angular one, and the inversion
manufactures the very crossing it exists to prevent: a near partner gets seated past
a far one and the two rays must cross. Straight ends now sort on `alongSideKey`;
step edges are untouched.

On punishing contact harder: tried, measured, rejected. Scaling the shared
crossing/overlap/crowding cost for straight edges makes the drawings WORSE, because
the router's only lever is bending around obstacle corners — at 2x one fixture goes
from 6 bends to 10 and its closest non-sibling approach from 35px to 0 (extra bends
land on shared corners), and at 4x from 6 to 19 bends with 41 pairs closer than 25px.
The shared weights stay. Left as a named `CONFLICT_SEVERITY` of 1 with that finding
recorded, so the next person does not repeat the experiment.

Handles: a straight edge has no segment to slide along a fixed axis, so the step
edge's elongated bend pill was the wrong metaphor — it is a polyline through POINTS
and editing it means picking a point up. Waypoints now render as round grips on the
points themselves with a move cursor, and EVERY interior vertex of the rendered route
carries one, including bends the router placed: dragging an automatic bend is how a
user takes ownership of a computed route. Segment midpoints keep a faint half-opacity
grip of the same shape — the same affordance, not real until dragged.

Full suite 1794 green; lint, build, size and knip pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4AtGeJaSWiGooNonpVcxN

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The rendered-route waypoint and live handle-preview fixes address two of the five requests. The 4.x migration, distant-checkpoint obstacle query, and bent include/extend and label geometry threads are still open, so I’m requesting changes on this SHA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

More Flexibility for adjusting paths between elements

2 participants