Support per-view connection boundary attachments in ordinary and state diagrams

This commit is contained in:
2026-09-22 17:51:13 -07:00
parent 3f9e6a18af
commit 79cc9d5551
49 changed files with 3534 additions and 52 deletions
+45 -2
View File
@@ -1785,14 +1785,16 @@ the Reparent command. Moving, resizing, arranging, or folding an owner retains
its authored sides and fractions. Frozen schema-1/schema-2 readers do not accept
<span class="docutils literal">port_anchors</span>; their explicit migration supplies an empty list.</p>
<p>Each <span class="docutils literal">routes</span> entry is an EdgeRoute record with a required <span class="docutils literal">edge</span> reference
to an existing Relation or Trace, <span class="docutils literal">waypoints</span> (default <span class="docutils literal">[]</span>), and <span class="docutils literal">label</span>
(default <span class="docutils literal">none</span>). Route records have no separate UUID or alias. An edge occurs
to an existing Relation or Trace, <span class="docutils literal">waypoints</span> (default <span class="docutils literal">[]</span>), <span class="docutils literal">label</span>
(default <span class="docutils literal">none</span>), and <span class="docutils literal">source_attachment</span> / <span class="docutils literal">target_attachment</span> (each
default <span class="docutils literal">none</span>). Route records have no separate UUID or alias. An edge occurs
at most once in a diagram's route list; different diagrams MAY store independent
routes for the same edge. List order and explicit empty entries are retained by
source and JSON round trips, although list order has no routing meaning. Edges
need not currently be visible in their diagram.</p>
<pre class="code text literal-block"><code>routes [
{ edge &#64;report_exchange;
source_attachment { side Bottom; offset 0.25; };
waypoints [{ x 420; y 260; }, { x 690; y 260; }];
label { fraction 0.55; offset { x 0; y -16; }; };
}
@@ -1809,6 +1811,47 @@ are structural errors. A pinned point that obstructs routing remains stored;
geometric routing difficulty is reported separately from structural validity.
Each EdgeRoute counts as one record against <span class="docutils literal">ParseLimits.max_records</span>; pinned
points are bounded by the per-route limit and aggregate input byte limits.</p>
<p>An attachment is an EdgeAttachment record with required <span class="docutils literal">side</span> (Left, Right,
Top, Bottom) and required finite binary32 <span class="docutils literal">offset</span> from zero through one.
Unknown fields, omitted nested fields and out-of-range offsets are structural
errors (<span class="docutils literal">invalid_attachment_offset</span> for the latter). Attachments are bounded
inline values, at most two per route, not additional graph objects or records.
JSON uses the same field names, enum strings and number values. Omitted or null
attachments mean automatic positioning; canonical compact source and JSON omit
absent attachments. Earlier schema-3 input hydrates both fields as absent.</p>
<p>The end names designate stored source/target slots, even for undirected edges.
These values MUST NOT change semantic endpoints, ports, ownership, type,
orientation or identity. Each preference belongs to that edge end in that view:
another view remains independent. It follows the current endpoint's projected
boundary under movement, resize or semantic reconnect; reconnect preserves the
preference, as it preserves authored bends. The fraction does not identify the
previous semantic endpoint. Deletion/reidentification of the edge follows the
existing whole-route rules; moving, hiding or replacing its placement retains
the preference. Removing one attachment restores that end's automatic position
without changing bends, labels or the opposite attachment.</p>
<p>For Left/Right, offset gives the Y fraction of the complete projected bounding
box; for Top/Bottom it gives the X fraction. The attachment intersects the painted
shape along that cardinal scan line. Rounded cards use radius 10, limited by half
their width/height; Regions and fork/join bars are rectangles. State circles use
their ellipse boundary and Choice uses its diamond boundary. More precisely, let
<span class="docutils literal">t = 2 * offset - 1</span> and <span class="docutils literal">D</span> be width for Left/Right or height for Top/Bottom.
The inset from the selected bounding side is <span class="docutils literal">D/2 * (1 - <span class="pre">sqrt(1-t*t))</span></span> for an
ellipse and <span class="docutils literal">D/2 * abs(t)</span> for a diamond. A Terminate X uses the outermost
painted-stroke intersection, inset <span class="docutils literal">D * min(offset, <span class="pre">1-offset)</span></span>. Zero and one
are valid; different side/fraction pairs can describe the same painted point.
Routing leads extend in the selected cardinal direction beyond the bounding box
before turning. Source/API fractions retain their precision; pointer editing
snaps fractions to hundredths without changing the stored semantic record.</p>
<p>An actual semantic Port remains authoritative, including when projection hides
its port marker. An edge attachment on that end is retained but dormant; use
the port's own placement anchor to move a port. When folding promotes an endpoint
to an ancestor, all authored route data is suspended and resumes on expansion.
Geometric difficulty MUST NOT discard preferences. Coincident authored self-loop
tips retain their positions with an external loop and a routing warning; the
editor must offer an explicit way to choose either end even when handles overlap.
Internal state transitions have no external connection. Process's generated
topology and Scenario's lifeline/activation/time-derived tips are not consumers
of these diagram route fields; this feature does not redefine their semantics.</p>
<p><span class="docutils literal">SetEdgeRoute { diagram, route }</span> upserts an entry by edge identity, retaining
the order and contents of unrelated entries. <span class="docutils literal">RemoveEdgeRoute { diagram, edge }</span>
removes it; removing an absent entry is a no-op for an existing diagram and edge.