patterns/_diagram-shared/
Canonical source plane for support code that more than one diagram engine consumes: text
layout and pointer input, each for the target set its row below declares. Files here are
authored once and mirrored out by
tools/sync-diagram-shared.mjs.
on this page
The plane confers no authority
_diagram-shared is a location, not a mandate. Each member declares its own explicit
target set, and that declaration — not this folder's name — decides which patterns
consume it. A future member may serve a different set, or one pattern only.
This matters because the owner's Class A static family is five patterns —
diagram-static-H, diagram-static-V, diagram-static-SEQ, diagram-static-FLOW and
diagram-static-peer — and FLOW and peer each carry a different grammar. A member that
governs only some of them must say so, or a later maintainer reads an absence as partial
vendoring and "completes" it.
Current members
| Member | Target set | Excluded |
|---|---|---|
Memberdiagrams-text-layout.js |
Target setdiagram-static-H · diagram-static-V · diagram-static-SEQ |
Excludeddiagram-static-FLOW · diagram-static-peer · diagram-interactive-spine · diagram-interactive-radial |
Memberdiagrams-pointer.js |
Target setdiagram-interactive-radial · diagram-interactive-spine · diagram-static-H · diagram-static-V · diagram-static-SEQ · diagram-static-FLOW |
Excludeddiagram-static-peer |
Canonical and mirrors
canonical patterns/_diagram-shared/diagrams-text-layout.js
mirrors patterns/diagram-static-{H,V,SEQ}/diagrams-text-layout.js
canonical patterns/_diagram-shared/diagrams-pointer.js
mirrors patterns/diagram-interactive-{radial,spine}/diagrams-pointer.js
patterns/diagram-static-{H,V,SEQ,FLOW}/diagrams-pointer.js
A mirror is byte-identical to its canonical and is a generated artifact.
node tools/sync-diagram-shared.mjs # emit node tools/sync-diagram-shared.mjs --check # exit 1 on any missing or divergent mirror
Never hand-edit a mirror. The mirror exists so a consumer vendors one self-contained
bundle directory rather than reaching across the repo; --check exists so a hand edit
fails loudly instead of surviving as a silent fork. Edit the canonical and re-emit.
What a member owns, and what it must not
diagrams-pointer.js owns gesture recognition for mouse, pen and touch on Pointer Events: pan past
a tap slop, a centroid pinch, a moved gesture that swallows its click, and the wheel. It does not
own a camera, a hit test or a selection: the consuming engine supplies its view, applies every
change and decides what a tap means. Its gesture logic is a device-approved controller moved as
donor code, with two declared corrections (a horizontal scroll no longer zooms; the pointer left
after a pinch keeps its own type). Its own header carries the attach contract.
v2 (2026-10-08) serves the static engines too. A static canvas holds the panels laid over the
drawing, so the caller names them in exclude: a press there stays native and a wheel there scrolls
unless it is a trackpad pinch. The controller marks the stage it owns with data-diagram-pointer,
which diagrams.css keys touch-action: none on. With exclude it also lays its own touch-action: none layer under the drawing and the panels, so
the panels keep the browser's own touch behavior, page zoom included. A moved gesture's click is
swallowed only when a pointer produced it, for at most 500 ms and never past the next press, so a
keyboard or programmatic activation — Enter on a linked card after a touch pan — and a tap on the
HUD straight after a pan are never eaten. Only a primary-button press starts a gesture, so a middle
click, a right click and a pen's barrel stay native.
The radial and spine engines pass no exclude, so they get no layer and keep their panels outside
the stage. Three v2 changes do reach them, all deliberate: a non-primary pen or touch press (a pen's
barrel) no longer pans; a moved gesture's click token now expires and never swallows a keyboard or
programmatic click (v1 swallowed the next click whatever produced it); and their stage carries
data-diagram-pointer. Their behavior suites pass against v2 unchanged.
diagrams-text-layout.js owns exact measurement, role metrics, cap application,
deterministic line breaking, wrapped height and tspan emission.
It does not own source grammar, topology, placement, connector geometry or the final SVG envelope. Those stay with each engine, because H, V and SEQ have genuinely different geometry contracts — a horizontal top-aligned cascade, a vertical band layout, and a linear sequence — and folding them together would produce a fourth engine wearing three names.
Measurement parity is the first gate. A string that does not wrap must measure exactly as it did before this plane existed, letter-spacing compensation included. That is what lets a no-wrap page prove geometric identity rather than merely look unchanged — and it held on every page of the measured fleet.
It is not a blanket render-neutrality guarantee, and two V tree shapes change with no wrapping at all. Both are corrections to notes V measured and never drew:
V section with a note the note no longer inflates band WIDTH (base: ~+434px for
text that was never drawn)
V group with a note the note is now DRAWN, and the group label takes the paired
offset — base selected BOX_H_NOTE for it and drew nothing
Nothing in the current fleet authors either shape, so no existing page moves. A downstream
V diagram that does author one will render differently after re-vendoring, and should:
the published grammar permits note on a group and on a section, and the old behaviour
spent geometry on invisible text.
These are the two known structural exceptions to no-wrap neutrality. Every measured current no-wrap page remains byte-identical. Inputs that exceed a role cap still change by design — wrapping them is the entire point of the contract — so "render-neutral" is a statement about the no-wrap path, never about the change as a whole.
What the helper does not own — and the trap that follows
The helper returns added height (addedHeight is 0 when a run does not wrap). The consuming engine
adds that to its own box, which means the engine, not the helper, owns where a wrapped run is
anchored.
That split has one sharp edge, and every consumer hits it: once a box has grown, a run anchored to the box's bottom or centre must subtract its own growth. Anchoring the first baseline to a grown edge deposits the new height as dead space at one end and pushes the run's remaining lines out the other. A run anchored to the box top needs no correction, because it grows in the same direction the box did.
The failure is quiet: nothing collides, nothing leaves the viewBox, and the text wraps correctly — it simply renders outside its own box. A containment assertion is the only check that sees it.
What the helper owns
line breaking delimiter matching, longest-first, and force-break measurement wrapped width and height for a given role cap role metrics per-role cap, line height, and the has-note predicate tspan emission the emitted text structure for a wrapped label or note
An engine that keeps a private copy of any of those is the divergence this file exists to remove. That is not a style preference, and the historical case is measured rather than recalled. V's local predicate counted a note on any kind while V drew one on neither a section nor a group:
base V, note on a GROUP BOX_H_NOTE selected · box 26 -> 44px · +18px height · +511px width
base V, note on a SECTION no height change at all — a section takes SECTION_H / SECTION_H_TAG,
never BOX_H_NOTE — but +434px of band WIDTH
both the note is never drawn
Two different symptoms from one drifted predicate. It is now resolved here and is target-aware:
H and V draw a section as label + rule + tag and never its note, while SEQ has no section branch at
all, so a kind: 'section' record there is an ordinary node that does render its note.
Engines keep what is genuinely theirs: source grammar, topology, base box geometry, placement, anchoring, connectors, the final envelope — and fonts and letter-spacing, which are CSS-derived and belong with the stylesheet.
The break contract
authored \n hard break, always delimiters matched LONGEST-FIRST — // before / — breaking AFTER the delimiter whitespace a break opportunity a token still over its cap force-broken, and only then
A visual line boundary does not corrupt an identifier: no source character is inserted, removed,
reordered or normalized. The helper returns two parallel forms — segments, the exact partition
where segments.join('') === source proves exact runtime string equality, and lines, the rendered payload,
which drops only trailing whitespace at a boundary so a text-anchor: middle run centres on its
glyphs. Keeping them apart is what lets the preservation gate assert exact identity without
pushing stray whitespace into the DOM.
That is a claim about the runtime JavaScript string, not about file bytes: two different literal
spellings or encodings can produce the same runtime string. Source-file byte preservation is a
separate fact, established because every *.source.js path is outside this change.