stdraw 0.7.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stdraw/__init__.py +32 -0
- stdraw/__main__.py +350 -0
- stdraw/_interactive_js.py +2030 -0
- stdraw/anchors.py +103 -0
- stdraw/bowtielayout.py +350 -0
- stdraw/check.py +651 -0
- stdraw/columnlayout.py +531 -0
- stdraw/comblayout.py +533 -0
- stdraw/crossing_exact.py +1004 -0
- stdraw/delivery.py +81 -0
- stdraw/dsl.py +99 -0
- stdraw/layout3d.py +167 -0
- stdraw/layout_auto.py +1827 -0
- stdraw/layoutdiff.py +424 -0
- stdraw/loops.py +145 -0
- stdraw/metrics.py +1225 -0
- stdraw/model.py +134 -0
- stdraw/placement.py +220 -0
- stdraw/planar.py +625 -0
- stdraw/recipes.py +68 -0
- stdraw/regularity.py +176 -0
- stdraw/render.py +361 -0
- stdraw/render3d.py +165 -0
- stdraw/rendercheck.py +90 -0
- stdraw/ringlayout.py +475 -0
- stdraw/routing.py +2630 -0
- stdraw/spinewinglayout.py +329 -0
- stdraw/symlayout.py +245 -0
- stdraw/textmetrics.py +97 -0
- stdraw/visibility.py +382 -0
- stdraw-0.7.13.dist-info/METADATA +383 -0
- stdraw-0.7.13.dist-info/RECORD +36 -0
- stdraw-0.7.13.dist-info/WHEEL +5 -0
- stdraw-0.7.13.dist-info/entry_points.txt +2 -0
- stdraw-0.7.13.dist-info/licenses/LICENSE +21 -0
- stdraw-0.7.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2030 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# ═══ Interactive layer script (drag rearrange; re-routing + bend handles;
|
|
4
|
+
# per-edge style cycle + free fold points + apply API) ═══
|
|
5
|
+
# Pure vanilla JS (zero dependencies, zero network). Attached to pages generated
|
|
6
|
+
# by render_auto_html:
|
|
7
|
+
# - drag nodes (SVG pointer events, mouse/touch unified; clamped to the
|
|
8
|
+
# viewBox while dragging);
|
|
9
|
+
# - live "all line kinds re-routed while dragging" (preview caliber):
|
|
10
|
+
# · straight/vg polylines: a JS port of the visibility-graph
|
|
11
|
+
# shortest-path router (same mathematical caliber as
|
|
12
|
+
# stdraw/routing+visibility: straight-line candidates need 16px
|
|
13
|
+
# clearance, otherwise inflated-corner visibility graph Dijkstra +
|
|
14
|
+
# string-pulling shortcuts, inflation 17.5px) — bends recomputed on
|
|
15
|
+
# every drag, no more pinned midpoints (fixes the root cause of
|
|
16
|
+
# the user-reported issue 2: the old reanchor only rebuilt endpoints and
|
|
17
|
+
# waypoints stayed pinned in place);
|
|
18
|
+
# · arcs: quadratic Bezier endpoint re-anchoring + control point
|
|
19
|
+
# translated by the mean endpoint displacement, all vertices follow;
|
|
20
|
+
# · unrelated edges pressed by the moving node (1px margin) also
|
|
21
|
+
# trigger re-routing;
|
|
22
|
+
# - draggable bend handles: each bend of a vg polyline (route.via non-empty)
|
|
23
|
+
# renders as a small circular handle (move cursor on hover); dragging it
|
|
24
|
+
# marks the edge "user-anchored" — afterwards node drags only re-anchor
|
|
25
|
+
# endpoints and keep user waypoints; the exported layout JSON carries
|
|
26
|
+
# edges[].waypoints, and CLI --from-layout recomputes under the same
|
|
27
|
+
# caliber (anchors = box border facing the first/last waypoint);
|
|
28
|
+
# - v0.7.7 per-edge style cycle: double-click a line to cycle
|
|
29
|
+
# auto -> straight -> arc -> free -> auto (>=10px hover hit zone,
|
|
30
|
+
# pointer cursor + highlight; double-click never disturbs node drags);
|
|
31
|
+
# forced states re-route live and the top-bar metrics recompute
|
|
32
|
+
# honestly (red when a hard gate fails — no leniency);
|
|
33
|
+
# - v0.7.7 free fold points: in free mode click the line to insert a
|
|
34
|
+
# handle (exact WYSIWYG polyline, no smoothing; endpoints re-anchored
|
|
35
|
+
# on the box borders facing the first/last fold point); at most
|
|
36
|
+
# MAX_FREE_PTS user fold points per edge — at the cap a click selects
|
|
37
|
+
# the nearest handle with a "max N points per edge" tooltip;
|
|
38
|
+
# - segment drag (drawio/bpmn-io .djs-segment-dragger caliber): on edges
|
|
39
|
+
# with explicit user geometry (free mode with fold points / user-
|
|
40
|
+
# anchored pinned polylines) dragging a segment translates the whole
|
|
41
|
+
# segment; jog bendpoints are created automatically at the dragged
|
|
42
|
+
# segment's pre-drag endpoints on first movement (anchor-adjacent
|
|
43
|
+
# segments insert one, interior segments insert two — the anchors
|
|
44
|
+
# themselves re-anchor on the box borders). Cursor reflects the segment
|
|
45
|
+
# orientation (ns-resize on horizontal, ew-resize on vertical).
|
|
46
|
+
# Auto-layout edges and straight lines are NOT affected: they keep live
|
|
47
|
+
# re-routing (deterministic contract untouched);
|
|
48
|
+
# - floating bendpoints (bpmn-io .djs-bendpoint.floating caliber): a
|
|
49
|
+
# semi-transparent handle parks at the MIDDLE of the hovered segment
|
|
50
|
+
# (mid-parking, not cursor-following — a cursor-following handle would
|
|
51
|
+
# intercept every pointerdown and make segment drag unreachable).
|
|
52
|
+
# Preview-only: hovering never writes user state. Dragging it >=4px
|
|
53
|
+
# away adopts the edge's current geometry as user fold points (free
|
|
54
|
+
# mode) and creates exactly one fold point; a plain click is a no-op;
|
|
55
|
+
# - fold-point lifecycle: a fold point released within 6px (svg units)
|
|
56
|
+
# of the polyline through its neighboring points merges back into the
|
|
57
|
+
# line (removed; pinned edges whose interior empties fall back to
|
|
58
|
+
# auto). Deletion semantics finalized = drag-back-to-line merge.
|
|
59
|
+
# Legacy paths (double-click style cycle / free click-insert / handle
|
|
60
|
+
# drag) unchanged. Drag preview: the live re-route + release-persist
|
|
61
|
+
# model is kept — no ghost layer added (documented no-op);
|
|
62
|
+
# - v0.7.7 persistence: "Export layout" writes edges[].style (+ fold
|
|
63
|
+
# points as waypoints); window.__stdrawApplyLayout({nodes, edge_styles,
|
|
64
|
+
# waypoints}) restores a layout programmatically; every drag release /
|
|
65
|
+
# fold-point insert / style change / reset dispatches the CustomEvent
|
|
66
|
+
# "stdraw:layoutchanged" on window (detail = the current layout), so
|
|
67
|
+
# product integrations can drop pointerup polling;
|
|
68
|
+
# - live metrics in the top bar (truly computed on preview geometry:
|
|
69
|
+
# crossings / 1px card presses / card gap / CV / row alignment);
|
|
70
|
+
# - full metric refresh on release + deterministic replay of R/B polarity
|
|
71
|
+
# markers (same caliber as render._polarity_marker);
|
|
72
|
+
# - v0.7.9 font-environment robustness: after render, the ACTUAL DOM text
|
|
73
|
+
# geometry is measured (getComputedTextLength/getBBox) per card; when the
|
|
74
|
+
# rendered text overflows the planned card rect the rect is grown (never
|
|
75
|
+
# shrunk) to fit and every edge re-anchors on the new border -- when the
|
|
76
|
+
# browser inflates DOM text (minimum font size / user font scale, which
|
|
77
|
+
# the engine-side text metrics cannot see) lines still hug the rendered
|
|
78
|
+
# frame at 0px. No-op when measured geometry matches the plan (initial
|
|
79
|
+
# render stays pixel-identical to the static rendering); public hook:
|
|
80
|
+
# window.__stdrawSyncDomGeometry();
|
|
81
|
+
# - "Reset" restores the initial layout (incl. original path_d rendering,
|
|
82
|
+
# handle removal, style + fold-point clearing).
|
|
83
|
+
# - t_2c2496ca standard anchors + endpoint drag: hover a box shows its 16
|
|
84
|
+
# standard anchors (stdraw/anchors.py caliber, toggleable); every edge's
|
|
85
|
+
# start/arrow endpoint is individually draggable (topmost #stdraw-eps hit
|
|
86
|
+
# circles beat the segment hit band), releasing snaps to the nearest
|
|
87
|
+
# standard anchor of the endpoint's own box, and clicking a visible
|
|
88
|
+
# anchor explicitly assigns it (nearest incident endpoint). Explicit
|
|
89
|
+
# designations (userAnchored) travel with the export (edges[].anchors)
|
|
90
|
+
# and __stdrawApplyLayout edge_anchors; afterwards node drags only
|
|
91
|
+
# RE-PROJECT the endpoint onto the SAME anchor position (never drift).
|
|
92
|
+
# The static layout / auto-routing path is untouched (no default-path
|
|
93
|
+
# behavior change; the 21-input sweep stays byte-identical).
|
|
94
|
+
# Determinism: the generated HTML contains no timestamps/random numbers;
|
|
95
|
+
# without dragging the initial geometry is pixel-identical to the static render.
|
|
96
|
+
_INTERACTIVE_JS = r"""(function () {
|
|
97
|
+
'use strict';
|
|
98
|
+
var dataEl = document.getElementById('stdraw-data');
|
|
99
|
+
if (!dataEl) return;
|
|
100
|
+
var D = JSON.parse(dataEl.textContent);
|
|
101
|
+
var svg = document.getElementById('stdraw-svg');
|
|
102
|
+
var VB = D.viewBox, cardH = D.cardH;
|
|
103
|
+
var CL = 16, VG_C = 17.5, VEPS = 1e-6; // same caliber as routing.CLEARANCE / +VG_PAD
|
|
104
|
+
var nodes = {};
|
|
105
|
+
D.nodes.forEach(function (n) { nodes[n.id] = {x: n.x, y: n.y, w: n.w, h: cardH, bx: n.x, by: n.y, g: null}; });
|
|
106
|
+
svg.querySelectorAll('[data-node]').forEach(function (g) {
|
|
107
|
+
var id = g.getAttribute('data-node');
|
|
108
|
+
if (nodes[id]) nodes[id].g = g;
|
|
109
|
+
// t_2c2496ca: hovering a box shows its 16 standard anchors (toggleable).
|
|
110
|
+
// The anchor dots layer is pointer-events:none — the dots never steal the
|
|
111
|
+
// cursor, so hovering stays on the node group and the dots cannot flicker.
|
|
112
|
+
g.addEventListener('mouseenter', function () {
|
|
113
|
+
hoverNode = g.getAttribute('data-node');
|
|
114
|
+
renderAnchors();
|
|
115
|
+
});
|
|
116
|
+
g.addEventListener('mouseleave', function () {
|
|
117
|
+
if (hoverNode === g.getAttribute('data-node')) { hoverNode = null; renderAnchors(); }
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
function quad(p0, c, p1, n) {
|
|
121
|
+
var out = [];
|
|
122
|
+
for (var k = 0; k <= n; k++) {
|
|
123
|
+
var t = k / n, u = 1 - t;
|
|
124
|
+
out.push([u*u*p0[0] + 2*u*t*c[0] + t*t*p1[0], u*u*p0[1] + 2*u*t*c[1] + t*t*p1[1]]);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function cp(p) { return [p[0], p[1]]; }
|
|
129
|
+
var edges = D.edges.map(function (e) {
|
|
130
|
+
var pts = e.pts, arc = !!e.arc && !!e.ctrl && pts.length >= 2;
|
|
131
|
+
if (arc) pts = quad(pts[0], e.ctrl, pts[pts.length - 1], 12);
|
|
132
|
+
pts = pts.map(cp);
|
|
133
|
+
return {a: e.a, b: e.b, pol: e.pol, arc: arc,
|
|
134
|
+
ctrl0: arc ? cp(e.ctrl) : null,
|
|
135
|
+
pa0: cp(pts[0]), pb0: cp(pts[pts.length - 1]),
|
|
136
|
+
via: (e.via && e.via.length >= 3) ? e.via.map(cp) : null,
|
|
137
|
+
corners: (e.via && e.via.length >= 3) ? e.via.map(cp) : null,
|
|
138
|
+
pts: pts, base: pts.map(cp), d0: null};
|
|
139
|
+
});
|
|
140
|
+
edges.forEach(function (e, i) {
|
|
141
|
+
var g = visG(i);
|
|
142
|
+
if (g) {
|
|
143
|
+
var p0 = g.querySelectorAll('path')[0];
|
|
144
|
+
if (p0) {
|
|
145
|
+
e.d0 = p0.getAttribute('d');
|
|
146
|
+
bindEdgeHover(i, p0); // visible stroke sits on top of the hit band
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
var polGroups = {};
|
|
151
|
+
// R/B text badges removed (loop polarity is expressed by edge coloring; the
|
|
152
|
+
// static stroke attributes are kept while dragging); polGroups/refreshMarkers
|
|
153
|
+
// remain as empty implementations for old page-structure compatibility.
|
|
154
|
+
var incidentMap = {};
|
|
155
|
+
Object.keys(nodes).forEach(function (v) {
|
|
156
|
+
incidentMap[v] = [];
|
|
157
|
+
edges.forEach(function (e, i) { if (e.a === v || e.b === v) incidentMap[v].push(i); });
|
|
158
|
+
});
|
|
159
|
+
function edgeD(pts) {
|
|
160
|
+
return 'M ' + pts.map(function (p) { return p[0].toFixed(1) + ' ' + p[1].toFixed(1); }).join(' L ');
|
|
161
|
+
}
|
|
162
|
+
// ── v0.7.7 per-edge hover hit zones (>=10px, pointer cursor, highlight) ──
|
|
163
|
+
function bindEdgeHover(i, el) {
|
|
164
|
+
// Hover wiring shared by the hit path AND the visible line: in the real
|
|
165
|
+
// render the visible stroke sits on top of the 12px hit band, so edge
|
|
166
|
+
// hover events must be bound to both (the ±0.8px sliver on the visible
|
|
167
|
+
// line targets the visible path; the rest of the band targets the hit
|
|
168
|
+
// path). Derived state: highlight + floating bendpoint preview +
|
|
169
|
+
// segment-orientation cursor.
|
|
170
|
+
el.addEventListener('mouseenter', function () { hlEdge(i, true); });
|
|
171
|
+
el.addEventListener('mouseleave', function () {
|
|
172
|
+
hlEdge(i, false);
|
|
173
|
+
// NOTE: the inline segment cursor is deliberately NOT cleared here.
|
|
174
|
+
// Showing the floating handle under a stationary cursor makes the
|
|
175
|
+
// browser fire mouseleave right after mousemove (hover recompute);
|
|
176
|
+
// clearing there would wipe the just-set ns/ew-resize cursor. The
|
|
177
|
+
// cursor self-corrects on the next mousemove over any edge segment.
|
|
178
|
+
// Preview visibility is decided by the elementFromPoint interval
|
|
179
|
+
// below, not by boundary events (measured timing-dependent headless).
|
|
180
|
+
});
|
|
181
|
+
el.addEventListener('mousemove', function (ev) {
|
|
182
|
+
var p = svgPoint(ev);
|
|
183
|
+
if (!p) return;
|
|
184
|
+
lastFloatPos = [ev.clientX, ev.clientY];
|
|
185
|
+
showFloat(i, p);
|
|
186
|
+
if (segDraggable(i)) {
|
|
187
|
+
var sd = segAt(edges[i].pts, p);
|
|
188
|
+
if (sd) el.style.cursor = orientCursor(edges[i].pts, sd.k);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
var SVGNS_EARLY = 'http://www.w3.org/2000/svg';
|
|
193
|
+
var hitLayer = document.createElementNS(SVGNS_EARLY, 'g');
|
|
194
|
+
hitLayer.id = 'stdraw-hits';
|
|
195
|
+
svg.insertBefore(hitLayer, svg.firstChild);
|
|
196
|
+
edges.forEach(function (e, i) {
|
|
197
|
+
var h = document.createElementNS(SVGNS_EARLY, 'path');
|
|
198
|
+
h.setAttribute('class', 'stdraw-hit');
|
|
199
|
+
h.setAttribute('data-edge', i);
|
|
200
|
+
h.setAttribute('d', edgeD(e.pts));
|
|
201
|
+
bindEdgeHover(i, h);
|
|
202
|
+
hitLayer.appendChild(h);
|
|
203
|
+
});
|
|
204
|
+
// ── v0.7.11 edge-element resolution: visible edges are `g[data-edge]`
|
|
205
|
+
// groups (render.py); hit paths are `path.stdraw-hit[data-edge]` inside
|
|
206
|
+
// #stdraw-hits, which sits at svg.firstChild — so a bare attribute-selector
|
|
207
|
+
// lookup on the data-edge attribute resolves to the HIT path first
|
|
208
|
+
// (document order). updateEdge then queried `path` CHILDREN of a path
|
|
209
|
+
// element (always empty) and the visible line never redrew: internal state
|
|
210
|
+
// re-anchored, DOM stale, zero exceptions (2026-11-08 8777/production regression,
|
|
211
|
+
// t_3fb02650). Always resolve the visible group / hit path explicitly.
|
|
212
|
+
function visG(i) { return svg.querySelector('g[data-edge="' + i + '"]'); }
|
|
213
|
+
function hitP(i) { return hitLayer.querySelector('.stdraw-hit[data-edge="' + i + '"]'); }
|
|
214
|
+
// ── v0.7.11 per-edge failure isolation (fail-visible, never abort the round):
|
|
215
|
+
// one edge whose reroute/updateEdge throws (degenerate geometry after card
|
|
216
|
+
// growth: coincident anchors, infeasible route) must not kill the remaining
|
|
217
|
+
// edges of a re-anchor pass — they would stay on stale anchors (jacky's Edge
|
|
218
|
+
// mixed floating-arrow outage, t_3fb02650). Degrade that edge visibly (red
|
|
219
|
+
// dashed stroke) + console WARN, keep going, and self-heal the marker on the
|
|
220
|
+
// edge's next successful pass.
|
|
221
|
+
function markDegraded(i, what, err) {
|
|
222
|
+
try { console.warn('[stdraw] edge #' + i + ' ' + what + ' failed: ' +
|
|
223
|
+
(err && err.message ? err.message : err)); } catch (e0) {}
|
|
224
|
+
var g = visG(i);
|
|
225
|
+
if (g) g.classList.add('stdraw-degraded');
|
|
226
|
+
var h = hitP(i);
|
|
227
|
+
if (h) h.classList.add('stdraw-degraded');
|
|
228
|
+
}
|
|
229
|
+
function clearDegraded(i) {
|
|
230
|
+
var g = visG(i);
|
|
231
|
+
if (g) g.classList.remove('stdraw-degraded');
|
|
232
|
+
var h = hitP(i);
|
|
233
|
+
if (h) h.classList.remove('stdraw-degraded');
|
|
234
|
+
}
|
|
235
|
+
function safeRerouteUpdate(i, what) {
|
|
236
|
+
try { reroute(i); updateEdge(i); clearDegraded(i); return true; }
|
|
237
|
+
catch (err) { markDegraded(i, what || 'reroute/update', err); return false; }
|
|
238
|
+
}
|
|
239
|
+
function hlEdge(i, on) {
|
|
240
|
+
var g = visG(i);
|
|
241
|
+
if (g) { if (on) g.classList.add('hl'); else g.classList.remove('hl'); }
|
|
242
|
+
setEpVis(i, on); // t_51392037: endpoint handle dots show only on hover
|
|
243
|
+
}
|
|
244
|
+
// t_51392037: per-edge endpoint handle visibility. Default hidden (visual
|
|
245
|
+
// noise in the rendered graph); visible while the edge is hovered or its
|
|
246
|
+
// endpoint is being dragged (drag hover is intercepted by the topmost hit
|
|
247
|
+
// circle, so the drag start must show them explicitly).
|
|
248
|
+
function setEpVis(i, on) {
|
|
249
|
+
var rec = epEls[i];
|
|
250
|
+
if (!rec) return;
|
|
251
|
+
[0, 1].forEach(function (end) {
|
|
252
|
+
(rec[end] || []).forEach(function (c) {
|
|
253
|
+
if (c.getAttribute('class').indexOf('stdraw-eps-hit') === -1)
|
|
254
|
+
c.classList.toggle('vis', !!on);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function updateEdge(i) {
|
|
259
|
+
var e = edges[i], g = visG(i);
|
|
260
|
+
var h = hitP(i);
|
|
261
|
+
if (h) h.setAttribute('d', edgeD(e.pts));
|
|
262
|
+
if (!g) return;
|
|
263
|
+
var paths = g.querySelectorAll('path');
|
|
264
|
+
if (paths[0]) paths[0].setAttribute('d', edgeD(e.pts));
|
|
265
|
+
if (paths[1]) {
|
|
266
|
+
var n = e.pts.length, a = e.pts[n - 2], b = e.pts[n - 1];
|
|
267
|
+
var ang = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
|
|
268
|
+
paths[1].setAttribute('transform', 'translate(' + b[0].toFixed(1) + ' ' + b[1].toFixed(1) + ') rotate(' + ang.toFixed(1) + ')');
|
|
269
|
+
}
|
|
270
|
+
updateEndpointHandles(i); // t_2c2496ca: endpoint handles track every redraw
|
|
271
|
+
}
|
|
272
|
+
function borderPoint(c, hw, hh, toward) {
|
|
273
|
+
// Connector delivery caliber: ray-determined border exit -> the anchor is
|
|
274
|
+
// clamped along that border's tangent into the edge-midpoint +/-1/4 edge
|
|
275
|
+
// length band (orthogonal exits land on the midpoint, diagonal exits are
|
|
276
|
+
// pulled back into the band, corners excluded).
|
|
277
|
+
// Same caliber as Python routing.border_point + Router.midband_reanchor.
|
|
278
|
+
var dx = toward[0] - c[0], dy = toward[1] - c[1];
|
|
279
|
+
if (!dx && !dy) return [c[0], c[1]];
|
|
280
|
+
if (!dx) return [c[0], c[1] + (dy > 0 ? hh : -hh)];
|
|
281
|
+
if (!dy) return [c[0] + (dx > 0 ? hw : -hw), c[1]];
|
|
282
|
+
var t = Math.min(hw / Math.abs(dx), hh / Math.abs(dy));
|
|
283
|
+
var px = c[0] + dx * t, py = c[1] + dy * t;
|
|
284
|
+
var onX = Math.abs(Math.abs(px - c[0]) - hw) <= Math.abs(Math.abs(py - c[1]) - hh);
|
|
285
|
+
if (onX) {
|
|
286
|
+
var limY = hh / 2;
|
|
287
|
+
return [px, c[1] + Math.max(-limY, Math.min(limY, py - c[1]))];
|
|
288
|
+
}
|
|
289
|
+
var limX = hw / 2;
|
|
290
|
+
return [c[0] + Math.max(-limX, Math.min(limX, px - c[0])), py];
|
|
291
|
+
}
|
|
292
|
+
// v0.7.9 per-card height: every border/obstacle/metrics site below uses the
|
|
293
|
+
// node's own h (synced to the rendered DOM geometry by syncDomGeometry)
|
|
294
|
+
// instead of the global planned cardH, so anchors always match the frame the
|
|
295
|
+
// user actually sees.
|
|
296
|
+
function nHH(n) { return (n.h || cardH) / 2; }
|
|
297
|
+
function anchor(n, toward) { return borderPoint([n.x, n.y], n.w / 2, nHH(n), toward); }
|
|
298
|
+
// ── t_2c2496ca standard per-box anchor set (16 points) ──
|
|
299
|
+
// Mirror of stdraw/anchors.py (authoritative Python caliber; the probe
|
|
300
|
+
// cross-checks JS vs Python equality). All 16 points sit on the border
|
|
301
|
+
// rectangle inset by ANCHOR_INSET (the routing fan-slot -3 inner-offset
|
|
302
|
+
// caliber): 4 edge midpoints + 4 corner points (never the geometric tip /
|
|
303
|
+
// the rounded rx=7 frame corner) + 2 golden-section points per edge
|
|
304
|
+
// (0.382 / 0.618, clockwise g1/g2 direction — see anchors.py docstring).
|
|
305
|
+
var ANCHOR_INSET = 3.0, GOLDEN1 = 0.382, GOLDEN2 = 0.618;
|
|
306
|
+
var ANCHOR_IDS = ['n', 'e', 's', 'w',
|
|
307
|
+
'ne', 'se', 'sw', 'nw',
|
|
308
|
+
'n-g1', 'n-g2', 'e-g1', 'e-g2',
|
|
309
|
+
's-g1', 's-g2', 'w-g1', 'w-g2'];
|
|
310
|
+
function stdAnchors(n) {
|
|
311
|
+
var a = n.w / 2 - ANCHOR_INSET, b = nHH(n) - ANCHOR_INSET;
|
|
312
|
+
var cx = n.x, cy = n.y;
|
|
313
|
+
function r6(v) { return Math.round(v * 1e6) / 1e6; }
|
|
314
|
+
return [['n', cx, cy - b], ['e', cx + a, cy], ['s', cx, cy + b], ['w', cx - a, cy],
|
|
315
|
+
['ne', cx + a, cy - b], ['se', cx + a, cy + b],
|
|
316
|
+
['sw', cx - a, cy + b], ['nw', cx - a, cy - b],
|
|
317
|
+
['n-g1', cx - a + 2 * a * GOLDEN1, cy - b],
|
|
318
|
+
['n-g2', cx - a + 2 * a * GOLDEN2, cy - b],
|
|
319
|
+
['e-g1', cx + a, cy - b + 2 * b * GOLDEN1],
|
|
320
|
+
['e-g2', cx + a, cy - b + 2 * b * GOLDEN2],
|
|
321
|
+
['s-g1', cx + a - 2 * a * GOLDEN1, cy + b],
|
|
322
|
+
['s-g2', cx + a - 2 * a * GOLDEN2, cy + b],
|
|
323
|
+
['w-g1', cx - a, cy + b - 2 * b * GOLDEN1],
|
|
324
|
+
['w-g2', cx - a, cy + b - 2 * b * GOLDEN2]
|
|
325
|
+
].map(function (p) { return [p[0], r6(p[1]), r6(p[2])]; });
|
|
326
|
+
}
|
|
327
|
+
function nearestAnchorId(v, px, py) {
|
|
328
|
+
var n = nodes[v];
|
|
329
|
+
if (!n) return null;
|
|
330
|
+
var pts = stdAnchors(n), best = null;
|
|
331
|
+
for (var k = 0; k < pts.length; k++) {
|
|
332
|
+
var d = Math.hypot(pts[k][1] - px, pts[k][2] - py);
|
|
333
|
+
if (best === null || d < best[0] - 1e-9) best = [d, pts[k][0]];
|
|
334
|
+
}
|
|
335
|
+
return best ? best[1] : null;
|
|
336
|
+
}
|
|
337
|
+
// userAnchored: edgeIdx -> {0: aid | 1: aid} — explicitly designated
|
|
338
|
+
// standard anchors (endpoint drag release / anchor click / applyLayout).
|
|
339
|
+
// user-anchored semantics: after designation, node drags only RE-PROJECT
|
|
340
|
+
// the endpoint onto the SAME anchor position (live geometry), never drift.
|
|
341
|
+
var userAnchored = {};
|
|
342
|
+
function userAnchorPt(i, end) {
|
|
343
|
+
var ua = userAnchored[i];
|
|
344
|
+
if (!ua || !ua[end]) return null;
|
|
345
|
+
var e = edges[i];
|
|
346
|
+
var n = nodes[end === 0 ? e.a : e.b];
|
|
347
|
+
if (!n) return null;
|
|
348
|
+
var pts = stdAnchors(n);
|
|
349
|
+
for (var k = 0; k < pts.length; k++) {
|
|
350
|
+
if (pts[k][0] === ua[end]) return [pts[k][1], pts[k][2]];
|
|
351
|
+
}
|
|
352
|
+
delete ua[end]; // stale designation: fall back gracefully
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function endPt(i, end, fallback) {
|
|
356
|
+
var p = userAnchorPt(i, end);
|
|
357
|
+
return p || fallback;
|
|
358
|
+
}
|
|
359
|
+
function reanchor(i) {
|
|
360
|
+
var e = edges[i], A = nodes[e.a], B = nodes[e.b];
|
|
361
|
+
var pts = e.pts.map(cp);
|
|
362
|
+
var inner = pts.length > 2;
|
|
363
|
+
pts[0] = endPt(i, 0, anchor(A, inner ? pts[1] : [B.x, B.y]));
|
|
364
|
+
pts[pts.length - 1] = endPt(i, 1, anchor(B, inner ? pts[pts.length - 2] : [A.x, A.y]));
|
|
365
|
+
e.pts = pts;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── JS-side preview router (visibility-graph shortest path, same mathematical caliber as visibility.py) ──
|
|
369
|
+
function ptSegDist(px, py, a, b) {
|
|
370
|
+
var vx = b[0] - a[0], vy = b[1] - a[1], L2 = vx * vx + vy * vy;
|
|
371
|
+
if (L2 < VEPS) return Math.hypot(px - a[0], py - a[1]);
|
|
372
|
+
var t = Math.max(0, Math.min(1, ((px - a[0]) * vx + (py - a[1]) * vy) / L2));
|
|
373
|
+
return Math.hypot(px - (a[0] + t * vx), py - (a[1] + t * vy));
|
|
374
|
+
}
|
|
375
|
+
function ornt(a, b, c) {
|
|
376
|
+
var v = (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0]);
|
|
377
|
+
return Math.abs(v) < VEPS ? 0 : (v > 0 ? 1 : -1);
|
|
378
|
+
}
|
|
379
|
+
function segSegDist(p1, p2, p3, p4) {
|
|
380
|
+
var d1 = ornt(p3, p4, p1), d2 = ornt(p3, p4, p2),
|
|
381
|
+
d3 = ornt(p1, p2, p3), d4 = ornt(p1, p2, p4);
|
|
382
|
+
if (d1 !== d2 && d3 !== d4 && d1 !== 0 && d3 !== 0) return 0.0;
|
|
383
|
+
return Math.min(ptSegDist(p1[0], p1[1], p3, p4),
|
|
384
|
+
ptSegDist(p2[0], p2[1], p3, p4),
|
|
385
|
+
ptSegDist(p3[0], p3[1], p1, p2),
|
|
386
|
+
ptSegDist(p4[0], p4[1], p1, p2));
|
|
387
|
+
}
|
|
388
|
+
function segHitsRect(x1, y1, x2, y2, cx, cy, hw, hh) {
|
|
389
|
+
var dx = x2 - x1, dy = y2 - y1, t0 = 0.0, t1 = 1.0;
|
|
390
|
+
var pq = [[-dx, x1 - (cx - hw)], [dx, (cx + hw) - x1],
|
|
391
|
+
[-dy, y1 - (cy - hh)], [dy, (cy + hh) - y1]];
|
|
392
|
+
for (var k = 0; k < 4; k++) {
|
|
393
|
+
var p = pq[k][0], q = pq[k][1];
|
|
394
|
+
if (Math.abs(p) < VEPS) { if (q < 0) return false; }
|
|
395
|
+
else {
|
|
396
|
+
var r = q / p;
|
|
397
|
+
if (p < 0) { if (r > t1) return false; if (r > t0) t0 = r; }
|
|
398
|
+
else { if (r < t0) return false; if (r < t1) t1 = r; }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return t1 - t0 > VEPS;
|
|
402
|
+
}
|
|
403
|
+
function segRectDist(p1, p2, r) {
|
|
404
|
+
if (segHitsRect(p1[0], p1[1], p2[0], p2[1], r[0], r[1], r[2], r[3])) return 0.0;
|
|
405
|
+
var cx = r[0], cy = r[1], hw = r[2], hh = r[3];
|
|
406
|
+
var cs = [[cx-hw, cy-hh], [cx+hw, cy-hh], [cx+hw, cy+hh], [cx-hw, cy+hh]];
|
|
407
|
+
var best = Infinity;
|
|
408
|
+
for (var k = 0; k < 4; k++) {
|
|
409
|
+
best = Math.min(best, segSegDist(p1, p2, cs[k], cs[(k + 1) % 4]));
|
|
410
|
+
}
|
|
411
|
+
return best;
|
|
412
|
+
}
|
|
413
|
+
function visible(p1, p2, rects, c) {
|
|
414
|
+
var t = c - VEPS;
|
|
415
|
+
var x1l = Math.min(p1[0], p2[0]), x1h = Math.max(p1[0], p2[0]);
|
|
416
|
+
var y1l = Math.min(p1[1], p2[1]), y1h = Math.max(p1[1], p2[1]);
|
|
417
|
+
for (var i = 0; i < rects.length; i++) {
|
|
418
|
+
var r = rects[i];
|
|
419
|
+
var gx = Math.max(r[0] - r[2] - x1h, x1l - r[0] - r[2], 0.0);
|
|
420
|
+
var gy = Math.max(r[1] - r[3] - y1h, y1l - r[1] - r[3], 0.0);
|
|
421
|
+
if (gx >= t || gy >= t) continue; // bbox distance lower bound >= c: cannot block
|
|
422
|
+
if (gx * gx + gy * gy >= t * t) continue;
|
|
423
|
+
if (segRectDist(p1, p2, r) < t) return false;
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
function vgShortest(src, dst, rects, c) {
|
|
428
|
+
if (!rects.length) return [cp(src), cp(dst)];
|
|
429
|
+
var nds = [cp(src), cp(dst)];
|
|
430
|
+
rects.forEach(function (r) {
|
|
431
|
+
var gx = r[2] + c, gy = r[3] + c;
|
|
432
|
+
nds.push([r[0]-gx, r[1]-gy], [r[0]+gx, r[1]-gy], [r[0]+gx, r[1]+gy], [r[0]-gx, r[1]+gy]);
|
|
433
|
+
});
|
|
434
|
+
var n = nds.length, adj = [];
|
|
435
|
+
for (var k0 = 0; k0 < n; k0++) adj.push([]);
|
|
436
|
+
for (var i = 0; i < n; i++) {
|
|
437
|
+
for (var j = i + 1; j < n; j++) {
|
|
438
|
+
if (visible(nds[i], nds[j], rects, c)) {
|
|
439
|
+
var w = Math.hypot(nds[i][0]-nds[j][0], nds[i][1]-nds[j][1]);
|
|
440
|
+
adj[i].push([j, w]); adj[j].push([i, w]);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Deterministic Dijkstra: array scan for the minimum (ties break to the lower index), same input same output
|
|
445
|
+
var INF = Infinity, dist = [], prev = [], done = [];
|
|
446
|
+
for (var k1 = 0; k1 < n; k1++) { dist.push(INF); prev.push(-1); done.push(false); }
|
|
447
|
+
dist[0] = 0;
|
|
448
|
+
for (var it = 0; it < n; it++) {
|
|
449
|
+
var u = -1, best = INF;
|
|
450
|
+
for (var k2 = 0; k2 < n; k2++) if (!done[k2] && dist[k2] < best - VEPS) { best = dist[k2]; u = k2; }
|
|
451
|
+
if (u < 0) break;
|
|
452
|
+
done[u] = true;
|
|
453
|
+
if (u === 1) break;
|
|
454
|
+
for (var e2 = 0; e2 < adj[u].length; e2++) {
|
|
455
|
+
var v = adj[u][e2][0], nd = dist[u] + adj[u][e2][1];
|
|
456
|
+
if (nd < dist[v] - VEPS) { dist[v] = nd; prev[v] = u; }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (dist[1] >= INF) return null;
|
|
460
|
+
var path = [], cur = 1;
|
|
461
|
+
while (cur !== -1) { path.push(cp(nds[cur])); cur = prev[cur]; }
|
|
462
|
+
path.reverse();
|
|
463
|
+
return path;
|
|
464
|
+
}
|
|
465
|
+
function shortcut(path, rects, c) {
|
|
466
|
+
var pts = path.map(cp);
|
|
467
|
+
for (var round = 0; round < 3; round++) {
|
|
468
|
+
var changed = false, i = 1;
|
|
469
|
+
while (i < pts.length - 1) {
|
|
470
|
+
if (visible(pts[i - 1], pts[i + 1], rects, c)) { pts.splice(i, 1); changed = true; }
|
|
471
|
+
else i++;
|
|
472
|
+
}
|
|
473
|
+
if (!changed) break;
|
|
474
|
+
}
|
|
475
|
+
return pts;
|
|
476
|
+
}
|
|
477
|
+
function rectsFor(a, b) {
|
|
478
|
+
var out = [];
|
|
479
|
+
Object.keys(nodes).forEach(function (v) {
|
|
480
|
+
if (v === a || v === b) return;
|
|
481
|
+
var n = nodes[v];
|
|
482
|
+
out.push([n.x, n.y, n.w / 2, nHH(n)]);
|
|
483
|
+
});
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function previewRoute(i) {
|
|
487
|
+
// Preview-caliber routing: straight line when visible; otherwise
|
|
488
|
+
// visibility-graph detour (same math as the Python router, without fillet
|
|
489
|
+
// smoothing and mutual-pair separation — the certificate remains
|
|
490
|
+
// authoritative via CLI recomputation).
|
|
491
|
+
var e = edges[i], A = nodes[e.a], B = nodes[e.b];
|
|
492
|
+
var pa = endPt(i, 0, anchor(A, [B.x, B.y]));
|
|
493
|
+
var pb = endPt(i, 1, anchor(B, [A.x, A.y]));
|
|
494
|
+
var rects = rectsFor(e.a, e.b);
|
|
495
|
+
if (visible(pa, pb, rects, CL)) { e.pts = [pa, pb]; e.corners = null; return; }
|
|
496
|
+
var path = vgShortest(pa, pb, rects, VG_C);
|
|
497
|
+
if (path && path.length >= 2) {
|
|
498
|
+
e.pts = shortcut(path, rects, VG_C);
|
|
499
|
+
e.corners = (e.pts.length >= 3) ? e.pts.map(cp) : null;
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
reanchor(i); // fallback (unreachable): shape-preserving re-anchor
|
|
503
|
+
}
|
|
504
|
+
function arcFollow(i) {
|
|
505
|
+
var e = edges[i], A = nodes[e.a], B = nodes[e.b];
|
|
506
|
+
// v0.7.11 defensive: an arc edge missing its frozen control state (partial
|
|
507
|
+
// restore paths) must degrade to a straight line, not throw mid-round.
|
|
508
|
+
if (!e.ctrl0 || !e.pa0 || !e.pb0) { forcedStraight(i); return; }
|
|
509
|
+
var pa = endPt(i, 0, anchor(A, [B.x, B.y]));
|
|
510
|
+
var pb = endPt(i, 1, anchor(B, [A.x, A.y]));
|
|
511
|
+
var ctrl = [e.ctrl0[0] + ((pa[0] - e.pa0[0]) + (pb[0] - e.pb0[0])) / 2,
|
|
512
|
+
e.ctrl0[1] + ((pa[1] - e.pa0[1]) + (pb[1] - e.pb0[1])) / 2];
|
|
513
|
+
e.pts = quad(pa, ctrl, pb, 12);
|
|
514
|
+
e.corners = null;
|
|
515
|
+
}
|
|
516
|
+
// ── v0.7.7 forced geometries (per-edge style cycle; same math caliber as
|
|
517
|
+
// the Python replay in layout_auto._manual_edge_styles/_forced_arc_route) ──
|
|
518
|
+
function forcedStraight(i) {
|
|
519
|
+
var e = edges[i], A = nodes[e.a], B = nodes[e.b];
|
|
520
|
+
var pa = endPt(i, 0, anchor(A, [B.x, B.y]));
|
|
521
|
+
var pb = endPt(i, 1, anchor(B, [A.x, A.y]));
|
|
522
|
+
e.pts = [pa, pb];
|
|
523
|
+
e.corners = null;
|
|
524
|
+
}
|
|
525
|
+
function adaptiveArcK(d) {
|
|
526
|
+
// same parameterization as routing.adaptive_arc_k
|
|
527
|
+
var bulge = Math.min(6.0 + 0.06 * d, 40.0);
|
|
528
|
+
var k = 2.0 * bulge / Math.max(d, 1e-6);
|
|
529
|
+
return Math.max(0.12, Math.min(0.50, k));
|
|
530
|
+
}
|
|
531
|
+
function forcedArc(i) {
|
|
532
|
+
// anchors = box borders facing each other; curvature = chord-adaptive k;
|
|
533
|
+
// side = the sign whose sampled arc keeps more distance from unrelated
|
|
534
|
+
// boxes (deterministic tie -> +1). Same rule as the Python replay.
|
|
535
|
+
var e = edges[i], A = nodes[e.a], B = nodes[e.b];
|
|
536
|
+
var pa = endPt(i, 0, anchor(A, [B.x, B.y]));
|
|
537
|
+
var pb = endPt(i, 1, anchor(B, [A.x, A.y]));
|
|
538
|
+
var vx = pb[0] - pa[0], vy = pb[1] - pa[1], d = Math.hypot(vx, vy);
|
|
539
|
+
var k = adaptiveArcK(d), n = Math.max(d, 1e-6);
|
|
540
|
+
var ux = -vy / n, uy = vx / n;
|
|
541
|
+
var best = null;
|
|
542
|
+
[1, -1].forEach(function (s) {
|
|
543
|
+
var ctrl = [(pa[0] + pb[0]) / 2 + ux * k * d * s,
|
|
544
|
+
(pa[1] + pb[1]) / 2 + uy * k * d * s];
|
|
545
|
+
var pts = quad(pa, ctrl, pb, 12);
|
|
546
|
+
var mclr = null;
|
|
547
|
+
Object.keys(nodes).forEach(function (v) {
|
|
548
|
+
if (v === e.a || v === e.b) return;
|
|
549
|
+
var nn = nodes[v];
|
|
550
|
+
for (var q = 0; q < pts.length; q++) {
|
|
551
|
+
var dd = distRect(pts[q][0], pts[q][1], nn.x, nn.y, nn.w / 2, nHH(nn));
|
|
552
|
+
mclr = (mclr === null) ? dd : Math.min(mclr, dd);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
var key = (-(mclr === null ? 1e9 : mclr));
|
|
556
|
+
if (best === null || key < best[0]) best = [key, pts, ctrl];
|
|
557
|
+
});
|
|
558
|
+
e.pts = best[1];
|
|
559
|
+
e.corners = null;
|
|
560
|
+
}
|
|
561
|
+
function reanchorFree(i) {
|
|
562
|
+
// free mode: exact polyline over the user fold points; endpoints = box
|
|
563
|
+
// borders facing the first/last fold point (same caliber as waypoints)
|
|
564
|
+
var e = edges[i], A = nodes[e.a], B = nodes[e.b];
|
|
565
|
+
var wps = freeWps[i];
|
|
566
|
+
if (!wps || !wps.length) { forcedStraight(i); return; }
|
|
567
|
+
var pts = [endPt(i, 0, anchor(A, wps[0]))];
|
|
568
|
+
pts = pts.concat(wps.map(cp));
|
|
569
|
+
pts.push(endPt(i, 1, anchor(B, wps[wps.length - 1])));
|
|
570
|
+
e.pts = pts;
|
|
571
|
+
e.corners = null;
|
|
572
|
+
}
|
|
573
|
+
function reroute(i) {
|
|
574
|
+
// v0.7.11 fault-injection hook (probe-only, never set by UI code): lets the
|
|
575
|
+
// DOM-truth regression probe force a single-edge router failure to prove the
|
|
576
|
+
// per-edge isolation + fail-visible degradation (t_3fb02650 acceptance).
|
|
577
|
+
if (edges[i] && edges[i]._probeThrow) {
|
|
578
|
+
throw new Error('probe-injected router failure (edge #' + i + ')');
|
|
579
|
+
}
|
|
580
|
+
var st = styles[i] || 'auto';
|
|
581
|
+
if (st === 'free') { reanchorFree(i); return; } // user fold points: re-anchor endpoints only
|
|
582
|
+
if (pinned[i]) { reanchor(i); return; } // user-anchored: re-anchor endpoints only, keep waypoints
|
|
583
|
+
if (st === 'straight') { forcedStraight(i); return; }
|
|
584
|
+
if (st === 'arc') { forcedArc(i); return; }
|
|
585
|
+
if (edges[i].arc) { arcFollow(i); return; }
|
|
586
|
+
previewRoute(i);
|
|
587
|
+
// v0.7.11 defensive final guard (degenerate geometry after card growth:
|
|
588
|
+
// extreme wide/flat cards, coincident anchors, infeasible route): any
|
|
589
|
+
// non-finite coordinate that slipped through a router path degrades this
|
|
590
|
+
// edge to a straight border-to-border line instead of poisoning e.pts
|
|
591
|
+
// (downstream edgeD/updateEdge/metrics would throw and abort the round).
|
|
592
|
+
var pts = edges[i].pts;
|
|
593
|
+
for (var k = 0; k < pts.length; k++) {
|
|
594
|
+
if (!isFinite(pts[k][0]) || !isFinite(pts[k][1])) { forcedStraight(i); return; }
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── Bend handles (vg polylines / user-anchored edges) ──
|
|
599
|
+
var pinned = {}; // edgeIdx -> true (user dragged a handle = waypoints anchored)
|
|
600
|
+
// v0.7.7 per-edge style cycle: edgeIdx -> 'auto' | 'straight' | 'arc' | 'free'
|
|
601
|
+
// (auto = engine default routing; free = exact polyline on user fold points)
|
|
602
|
+
var styles = {};
|
|
603
|
+
var freeWps = {}; // edgeIdx -> [[x,y], ...] user fold points (free mode, max MAX_FREE_PTS)
|
|
604
|
+
// Segment-drag re-review (2026-11-08): an interior segment drag inserts TWO
|
|
605
|
+
// jog bendpoints per gesture and a realistic orthogonal detour (e.g. jogging
|
|
606
|
+
// around two cards on 8777 P5) needs 4-6 points; drawio/bpmn-io set no hard
|
|
607
|
+
// cap at all. 8 = headroom for two segment-drags on one edge while keeping
|
|
608
|
+
// metrics/metrics sampling and export/applyLayout truncation sane.
|
|
609
|
+
var MAX_FREE_PTS = 8; // per-edge user fold-point cap (creation-time only)
|
|
610
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
611
|
+
var styleEl = document.createElement('style');
|
|
612
|
+
styleEl.textContent = '#stdraw-handles circle.stdraw-wp{fill:#fff;stroke:#475569;' +
|
|
613
|
+
'stroke-width:1.4;cursor:move;pointer-events:all}' +
|
|
614
|
+
'#stdraw-handles circle.stdraw-wp:hover{stroke:#dc2626;fill:#fef2f2}' +
|
|
615
|
+
'#stdraw-handles circle.stdraw-wp.sel{stroke:#dc2626;fill:#fef2f2}' +
|
|
616
|
+
// Floating bendpoints (bpmn-io .djs-bendpoint.floating): preview-only
|
|
617
|
+
// handle; creating it never touches user state, so it is safe to render
|
|
618
|
+
// on hover and discard on leave.
|
|
619
|
+
// NOTE (t_b68f21be): the floating preview mounts in the #stdraw-float
|
|
620
|
+
// layer (floatLayer below), NOT #stdraw-handles -- rebuildHandles clears
|
|
621
|
+
// #stdraw-handles on every gesture and would kill the preview. Prefixing
|
|
622
|
+
// these rules with a layer id silently detached them (handle rendered as
|
|
623
|
+
// a black opaque dot on 8777). Keep the selector layer-agnostic: the
|
|
624
|
+
// probe pins computed opacity so a re-layering regression turns red.
|
|
625
|
+
'circle.stdraw-wp.floating{fill:#94a3b8;stroke:#475569;' +
|
|
626
|
+
'stroke-width:1.4;cursor:move;pointer-events:all;opacity:0.45}' +
|
|
627
|
+
'circle.stdraw-wp.floating:hover{opacity:0.85}' +
|
|
628
|
+
// Edge hit paths: the stroke MUST be painted (alpha > 0) for Chromium's
|
|
629
|
+
// pointer-events:stroke to hit-test it -- stroke:transparent is treated as
|
|
630
|
+
// unpainted and the hit path never resolves under the cursor (2026-11-08
|
|
631
|
+
// 8777 regression: all edge interactions dead while node drags worked).
|
|
632
|
+
// Same caliber as diagram-js InteractionEvents createHitStyle (painted
|
|
633
|
+
// near-transparent hit stroke, production-proven). Do NOT switch to
|
|
634
|
+
// pointer-events:all: with fill:none the implicit-closed fill polygon of
|
|
635
|
+
// an open polyline would hit-test its interior and steal empty-canvas
|
|
636
|
+
// clicks. rgba(...,0.01) is visually imperceptible (+<=2.5/255 composite).
|
|
637
|
+
'#stdraw-svg path.stdraw-hit{fill:none;stroke:rgba(255,255,255,0.01);stroke-width:12;cursor:pointer;pointer-events:stroke}' +
|
|
638
|
+
'#stdraw-svg [data-edge].hl > path:not(.stdraw-hit){stroke-width:2.4}' +
|
|
639
|
+
// v0.7.11 per-edge degradation marker (fail-visible): a single edge whose
|
|
640
|
+
// re-route threw keeps its last geometry but is drawn red + dashed so the
|
|
641
|
+
// failure is visible instead of silently floating.
|
|
642
|
+
'#stdraw-svg g[data-edge].stdraw-degraded > path:not(.stdraw-hit){stroke:#dc2626;stroke-dasharray:6 4}' +
|
|
643
|
+
'#stdraw-svg path.stdraw-hit.stdraw-degraded{stroke:rgba(220,38,38,0.01)}' +
|
|
644
|
+
// t_2c2496ca: standard-anchor dots (hover preview). pointer-events:none —
|
|
645
|
+
// the dots never intercept the cursor (click-to-assign is resolved by the
|
|
646
|
+
// svg pointerdown handler against the LIVE anchor coordinates instead, so
|
|
647
|
+
// hovering a dot keeps the node hover alive and the dots cannot flicker).
|
|
648
|
+
'#stdraw-anchors{pointer-events:none}' +
|
|
649
|
+
'#stdraw-anchors circle.stdraw-anchor{fill:#fff;stroke:#0284c7;stroke-width:1.1;opacity:0.9}' +
|
|
650
|
+
'#stdraw-anchors circle.stdraw-anchor.pinned{fill:#0284c7;stroke:#075985}' +
|
|
651
|
+
'#stdraw-anchors circle.stdraw-anchor.near{fill:#fbbf24;stroke:#d97706}' +
|
|
652
|
+
// t_2c2496ca: per-edge ENDPOINT drag handles. The 9px invisible hit circle
|
|
653
|
+
// sits in the TOPMOST layer (#stdraw-eps appended last), so an endpoint
|
|
654
|
+
// press always resolves to the endpoint drag BEFORE the 12px segment hit
|
|
655
|
+
// band (drag-line = whole-segment translation) can claim it — endpoint
|
|
656
|
+
// hit priority is structural (DOM order), pinned by the probe.
|
|
657
|
+
// t_51392037: the VISIBLE handle dot is hidden by default (jacky: dots in
|
|
658
|
+
// the rendered graph are visual noise); it fades in only while its edge is
|
|
659
|
+
// hovered or its endpoint is being dragged (.vis class, see setEpVis).
|
|
660
|
+
'#stdraw-eps circle.stdraw-ep{fill:#fff;stroke:#0284c7;stroke-width:1.6;cursor:move;pointer-events:all;opacity:0;transition:opacity .12s}' +
|
|
661
|
+
'#stdraw-eps circle.stdraw-ep.vis{opacity:1}' +
|
|
662
|
+
'#stdraw-eps circle.stdraw-eps-hit{fill:rgba(255,255,255,0);stroke:none;cursor:move;pointer-events:all}' +
|
|
663
|
+
'.stdraw-tip{position:absolute;z-index:9;background:#1f2937;color:#fff;' +
|
|
664
|
+
'font:12px/1.4 sans-serif;padding:3px 8px;border-radius:4px;pointer-events:none}';
|
|
665
|
+
document.head.appendChild(styleEl);
|
|
666
|
+
var handleLayer = document.createElementNS(NS, 'g');
|
|
667
|
+
handleLayer.id = 'stdraw-handles';
|
|
668
|
+
svg.appendChild(handleLayer);
|
|
669
|
+
function edgeHasHandles(i) {
|
|
670
|
+
var e = edges[i];
|
|
671
|
+
if ((styles[i] || 'auto') === 'free') return !!(freeWps[i] && freeWps[i].length);
|
|
672
|
+
if (e.arc) return false;
|
|
673
|
+
if (pinned[i]) return e.pts.length >= 3;
|
|
674
|
+
return !!(e.corners && e.corners.length >= 3);
|
|
675
|
+
}
|
|
676
|
+
function handlePts(i) { // handle positions: free = user fold points; unanchored = current corners; anchored = midpoints of pts
|
|
677
|
+
if ((styles[i] || 'auto') === 'free') return freeWps[i] || [];
|
|
678
|
+
return pinned[i] ? edges[i].pts : edges[i].corners;
|
|
679
|
+
}
|
|
680
|
+
function rebuildHandles() {
|
|
681
|
+
while (handleLayer.firstChild) handleLayer.removeChild(handleLayer.firstChild);
|
|
682
|
+
edges.forEach(function (e, i) {
|
|
683
|
+
if (!edgeHasHandles(i)) return;
|
|
684
|
+
var pts = handlePts(i);
|
|
685
|
+
if (!pts || pts.length < 1) return;
|
|
686
|
+
if ((styles[i] || 'auto') === 'free') {
|
|
687
|
+
// free-mode handles: one per user fold point (data-wp = fold index;
|
|
688
|
+
// pts index = fold index + 1)
|
|
689
|
+
for (var kf = 0; kf < pts.length; kf++) {
|
|
690
|
+
var cf = document.createElementNS(NS, 'circle');
|
|
691
|
+
cf.setAttribute('cx', pts[kf][0].toFixed(1));
|
|
692
|
+
cf.setAttribute('cy', pts[kf][1].toFixed(1));
|
|
693
|
+
cf.setAttribute('r', '4');
|
|
694
|
+
cf.setAttribute('class', 'stdraw-wp');
|
|
695
|
+
cf.setAttribute('data-edge', i);
|
|
696
|
+
cf.setAttribute('data-wp', kf + 1);
|
|
697
|
+
cf.setAttribute('data-free', '1');
|
|
698
|
+
handleLayer.appendChild(cf);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (pts.length < 3) return;
|
|
703
|
+
for (var k = 1; k < pts.length - 1; k++) {
|
|
704
|
+
var c = document.createElementNS(NS, 'circle');
|
|
705
|
+
c.setAttribute('cx', pts[k][0].toFixed(1));
|
|
706
|
+
c.setAttribute('cy', pts[k][1].toFixed(1));
|
|
707
|
+
c.setAttribute('r', '4');
|
|
708
|
+
c.setAttribute('class', 'stdraw-wp');
|
|
709
|
+
c.setAttribute('data-edge', i);
|
|
710
|
+
c.setAttribute('data-wp', k);
|
|
711
|
+
handleLayer.appendChild(c);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function pinEdge(i) {
|
|
716
|
+
if (pinned[i]) return;
|
|
717
|
+
var e = edges[i];
|
|
718
|
+
// User-anchored waypoints = midpoints of the current corners (endpoint
|
|
719
|
+
// anchors are recomputed on the box border by CLI/re-anchoring, and are not
|
|
720
|
+
// exported as waypoints — avoiding redundant bends coinciding with anchors)
|
|
721
|
+
var cs = (e.corners && e.corners.length >= 3) ? e.corners : e.pts;
|
|
722
|
+
var via = cs.slice(1, -1).map(cp);
|
|
723
|
+
if (via.length < 1) return;
|
|
724
|
+
var A = nodes[e.a], B = nodes[e.b];
|
|
725
|
+
// Same caliber as CLI --from-layout: anchor = box border facing the first/last waypoint
|
|
726
|
+
var pts = [endPt(i, 0, anchor(A, via[0]))];
|
|
727
|
+
pts = pts.concat(via);
|
|
728
|
+
pts.push(endPt(i, 1, anchor(B, via[via.length - 1])));
|
|
729
|
+
e.pts = pts;
|
|
730
|
+
e.corners = pts.map(cp);
|
|
731
|
+
pinned[i] = true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function segCross(p1, p2, p3, p4) {
|
|
735
|
+
function or(a, b, c) {
|
|
736
|
+
var v = (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0]);
|
|
737
|
+
return Math.abs(v) < 1e-9 ? 0 : (v > 0 ? 1 : -1);
|
|
738
|
+
}
|
|
739
|
+
var d1 = or(p3,p4,p1), d2 = or(p3,p4,p2), d3 = or(p1,p2,p3), d4 = or(p1,p2,p4);
|
|
740
|
+
return d1 !== d2 && d3 !== d4 && d1 !== 0 && d3 !== 0;
|
|
741
|
+
}
|
|
742
|
+
function distRect(px, py, cx, cy, hw, hh) {
|
|
743
|
+
var dx = Math.max(Math.abs(px - cx) - hw, 0), dy = Math.max(Math.abs(py - cy) - hh, 0);
|
|
744
|
+
return Math.hypot(dx, dy);
|
|
745
|
+
}
|
|
746
|
+
function samplePts(pts, step) {
|
|
747
|
+
step = step || 4;
|
|
748
|
+
var out = [];
|
|
749
|
+
for (var i = 0; i < pts.length - 1; i++) {
|
|
750
|
+
var a = pts[i], b = pts[i + 1], d = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
751
|
+
var n = Math.max(1, Math.floor(d / step));
|
|
752
|
+
for (var k = 0; k < n; k++) { var t = k / n; out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); }
|
|
753
|
+
}
|
|
754
|
+
out.push(pts[pts.length - 1].slice());
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
function metrics() {
|
|
758
|
+
var ids = Object.keys(nodes), crossings = 0;
|
|
759
|
+
for (var i = 0; i < edges.length; i++) {
|
|
760
|
+
for (var j = i + 1; j < edges.length; j++) {
|
|
761
|
+
var e1 = edges[i], e2 = edges[j];
|
|
762
|
+
if (e1.a === e2.a || e1.a === e2.b || e1.b === e2.a || e1.b === e2.b) continue;
|
|
763
|
+
var hit = false;
|
|
764
|
+
for (var s1 = 0; s1 < e1.pts.length - 1 && !hit; s1++) {
|
|
765
|
+
for (var s2 = 0; s2 < e2.pts.length - 1; s2++) {
|
|
766
|
+
if (segCross(e1.pts[s1], e1.pts[s1 + 1], e2.pts[s2], e2.pts[s2 + 1])) { hit = true; break; }
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (hit) crossings++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
var press = 0;
|
|
773
|
+
edges.forEach(function (e) {
|
|
774
|
+
var pts = samplePts(e.pts);
|
|
775
|
+
ids.forEach(function (v) {
|
|
776
|
+
if (v === e.a || v === e.b) return;
|
|
777
|
+
var n = nodes[v];
|
|
778
|
+
for (var k = 0; k < pts.length; k++) {
|
|
779
|
+
if (distRect(pts[k][0], pts[k][1], n.x, n.y, n.w / 2 + 1, nHH(n) + 1) <= 0) { press++; break; }
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
var minGap = Infinity, nn = [];
|
|
784
|
+
for (var a = 0; a < ids.length; a++) {
|
|
785
|
+
var na = nodes[ids[a]], nna = Infinity;
|
|
786
|
+
for (var b = 0; b < ids.length; b++) {
|
|
787
|
+
if (a === b) continue;
|
|
788
|
+
var nb = nodes[ids[b]];
|
|
789
|
+
var dgx = Math.abs(na.x - nb.x) - (na.w + nb.w) / 2;
|
|
790
|
+
var dgy = Math.abs(na.y - nb.y) - nHH(na) - nHH(nb);
|
|
791
|
+
var gg = Math.hypot(Math.max(dgx, 0), Math.max(dgy, 0));
|
|
792
|
+
if (gg < minGap) minGap = gg;
|
|
793
|
+
var dc = Math.hypot(na.x - nb.x, na.y - nb.y);
|
|
794
|
+
if (dc < nna) nna = dc;
|
|
795
|
+
}
|
|
796
|
+
nn.push(nna);
|
|
797
|
+
}
|
|
798
|
+
var mean = nn.reduce(function (s, x) { return s + x; }, 0) / nn.length;
|
|
799
|
+
var cv = Math.sqrt(nn.reduce(function (s, x) { return s + (x - mean) * (x - mean); }, 0) / nn.length) / mean;
|
|
800
|
+
var ys = ids.map(function (v) { return nodes[v].y; }).sort(function (p, q) { return p - q; });
|
|
801
|
+
var aligned = 0, i2 = 0;
|
|
802
|
+
while (i2 < ys.length) {
|
|
803
|
+
var j2 = i2;
|
|
804
|
+
while (j2 + 1 < ys.length && ys[j2 + 1] - ys[j2] <= 4) j2++;
|
|
805
|
+
if (j2 > i2) aligned += j2 - i2 + 1;
|
|
806
|
+
i2 = j2 + 1;
|
|
807
|
+
}
|
|
808
|
+
return {crossings: crossings, press: press, minGap: Math.round(minGap * 10) / 10,
|
|
809
|
+
cv: Math.round(cv * 1000) / 1000, row: Math.round(aligned / ids.length * 1000) / 1000};
|
|
810
|
+
}
|
|
811
|
+
function refreshMetrics() {
|
|
812
|
+
var el = document.getElementById('stdraw-metrics');
|
|
813
|
+
if (!el) return;
|
|
814
|
+
var m = metrics();
|
|
815
|
+
el.textContent = 'crossings ' + m.crossings + ' · presses(1px) ' + m.press +
|
|
816
|
+
' · card gap ' + m.minGap + 'px · CV ' + m.cv + ' · row align ' + m.row;
|
|
817
|
+
// honest hard-gate coloring (no leniency): red when a gate clearly fails
|
|
818
|
+
el.style.color = (m.press > 0 || m.crossings > 2 || m.minGap < 16) ? '#dc2626' : '';
|
|
819
|
+
}
|
|
820
|
+
// ── v0.7.9 font-environment robustness: post-render DOM geometry re-anchor ──
|
|
821
|
+
// The planned card geometry is produced by the engine-side text metrics layer
|
|
822
|
+
// (PIL measurement at generation time) and cannot see browser font settings.
|
|
823
|
+
// When the browser inflates the DOM text (minimum font size / user font
|
|
824
|
+
// scale), the rendered label overflows the planned rect while the edges stay
|
|
825
|
+
// anchored to the planned border -> floating arrows (jacky's Edge outage,
|
|
826
|
+
// t_30847af7). Here the ACTUAL rendered text is measured per card
|
|
827
|
+
// (getComputedTextLength + getBBox), the rect is grown (never shrunk) to fit
|
|
828
|
+
// with the same conservative padding discipline as textmetrics.py
|
|
829
|
+
// (>=12px per side + 6% stretch margin), the node geometry every router /
|
|
830
|
+
// metrics path consumes is updated, and all edges re-anchor + re-route on the
|
|
831
|
+
// new borders at 0px. Determinism: when the measured geometry fits inside the
|
|
832
|
+
// plan the function is a strict no-op (the initial render stays pixel-
|
|
833
|
+
// identical to the static rendering; the byte-frozen delivery contract is
|
|
834
|
+
// untouched). Deterministic given the rendered geometry: fixed traversal
|
|
835
|
+
// order, no RNG, no timestamps.
|
|
836
|
+
function syncDomGeometry() {
|
|
837
|
+
var touched = false;
|
|
838
|
+
svg.querySelectorAll('[data-node]').forEach(function (g) {
|
|
839
|
+
// v0.7.11 per-node isolation: a measurement failure on one card must not
|
|
840
|
+
// abort the growth pass for the remaining cards.
|
|
841
|
+
try {
|
|
842
|
+
var id = g.getAttribute('data-node'), n = nodes[id];
|
|
843
|
+
if (!n || !n.g) return;
|
|
844
|
+
var rect = n.g.querySelector('rect');
|
|
845
|
+
if (!rect) return;
|
|
846
|
+
var mw = 0, top = Infinity, bot = -Infinity, texts = g.querySelectorAll('text');
|
|
847
|
+
texts.forEach(function (t) {
|
|
848
|
+
var w = 0;
|
|
849
|
+
try { w = t.getComputedTextLength(); } catch (e1) {}
|
|
850
|
+
try {
|
|
851
|
+
var bb = t.getBBox();
|
|
852
|
+
if (bb.width > w) w = bb.width;
|
|
853
|
+
if (bb.y < top) top = bb.y;
|
|
854
|
+
if (bb.y + bb.height > bot) bot = bb.y + bb.height;
|
|
855
|
+
} catch (e2) {}
|
|
856
|
+
if (w > mw) mw = w;
|
|
857
|
+
});
|
|
858
|
+
if (!(mw > 0)) return;
|
|
859
|
+
// Grow-only + overflow-driven: the planned geometry (engine-side PIL text
|
|
860
|
+
// metrics + conservative padding) is authoritative when the rendered text
|
|
861
|
+
// already fits inside it (any blanket re-padding here would drift the
|
|
862
|
+
// deterministic initial render -- measured, not assumed).
|
|
863
|
+
var needW = n.w, needH = n.h;
|
|
864
|
+
var wNeed = Math.ceil(mw * 1.06 + 24); // textmetrics.py: >=12px/side + 6% stretch
|
|
865
|
+
if (wNeed > needW + 0.5) needW = wNeed;
|
|
866
|
+
if (top < bot && (top < n.by - n.h / 2 - 0.5 || bot > n.by + n.h / 2 + 0.5)) {
|
|
867
|
+
// vertical text escape: fit the union with the plan's padding discipline
|
|
868
|
+
var hNeed = Math.ceil((bot - top) * 1.06 + 14);
|
|
869
|
+
if (hNeed > needH) needH = hNeed;
|
|
870
|
+
}
|
|
871
|
+
if (needW <= n.w + 0.5 && needH <= n.h + 0.5) return; // fits: strict no-op
|
|
872
|
+
var grewH = needH > n.h + 0.5;
|
|
873
|
+
n.w = needW; n.h = needH;
|
|
874
|
+
rect.setAttribute('x', (n.bx - needW / 2).toFixed(1));
|
|
875
|
+
rect.setAttribute('y', (n.by - needH / 2).toFixed(1));
|
|
876
|
+
rect.setAttribute('width', String(needW));
|
|
877
|
+
rect.setAttribute('height', String(needH));
|
|
878
|
+
// keep the secondary (value) line pinned to the new bottom inset when the
|
|
879
|
+
// card grew taller (the label stays centered on the card middle)
|
|
880
|
+
if (grewH && texts.length >= 2) {
|
|
881
|
+
try { texts[texts.length - 1].setAttribute('y', (n.by + needH / 2 - 7).toFixed(1)); } catch (e3) {}
|
|
882
|
+
}
|
|
883
|
+
touched = true;
|
|
884
|
+
} catch (errN) {
|
|
885
|
+
try { console.warn('[stdraw] node #' + g.getAttribute('data-node') +
|
|
886
|
+
' geometry sync failed: ' + (errN && errN.message ? errN.message : errN)); } catch (e4) {}
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
if (touched) {
|
|
890
|
+
// v0.7.11 per-edge isolation: a single edge throwing must never abort the
|
|
891
|
+
// re-anchor round (stale anchors on all subsequent edges = the mixed
|
|
892
|
+
// floating-arrow state jacky saw in Edge). Fail-visible per edge instead.
|
|
893
|
+
edges.forEach(function (e, i) { safeRerouteUpdate(i, 'sync re-anchor'); });
|
|
894
|
+
try { rebuildHandles(); } catch (errH) {
|
|
895
|
+
try { console.warn('[stdraw] rebuildHandles failed: ' +
|
|
896
|
+
(errH && errH.message ? errH.message : errH)); } catch (e5) {}
|
|
897
|
+
}
|
|
898
|
+
try { refreshMetrics(); } catch (errM) {
|
|
899
|
+
try { console.warn('[stdraw] refreshMetrics failed: ' +
|
|
900
|
+
(errM && errM.message ? errM.message : errM)); } catch (e6) {}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return touched;
|
|
904
|
+
}
|
|
905
|
+
function polyPointAt(pts, t) {
|
|
906
|
+
var segs = [], total = 0, i;
|
|
907
|
+
for (i = 0; i < pts.length - 1; i++) {
|
|
908
|
+
var d = Math.hypot(pts[i+1][0]-pts[i][0], pts[i+1][1]-pts[i][1]);
|
|
909
|
+
segs.push(d); total += d;
|
|
910
|
+
}
|
|
911
|
+
var target = total * Math.max(0, Math.min(1, t)), acc = 0;
|
|
912
|
+
for (i = 0; i < segs.length; i++) {
|
|
913
|
+
if (acc + segs[i] >= target && segs[i] > 0) {
|
|
914
|
+
var f = (target - acc) / segs[i];
|
|
915
|
+
var p = [pts[i][0] + (pts[i+1][0]-pts[i][0]) * f, pts[i][1] + (pts[i+1][1]-pts[i][1]) * f];
|
|
916
|
+
return {p: p, tan: [(pts[i+1][0]-pts[i][0]) / segs[i], (pts[i+1][1]-pts[i][1]) / segs[i]]};
|
|
917
|
+
}
|
|
918
|
+
acc += segs[i];
|
|
919
|
+
}
|
|
920
|
+
return {p: pts[pts.length - 1].slice(), tan: [0, 1]};
|
|
921
|
+
}
|
|
922
|
+
function ptPolyDist(px, py, pts) {
|
|
923
|
+
var best = Infinity;
|
|
924
|
+
for (var i = 0; i < pts.length - 1; i++) {
|
|
925
|
+
var a = pts[i], b = pts[i + 1];
|
|
926
|
+
var vx = b[0]-a[0], vy = b[1]-a[1], L2 = vx*vx + vy*vy;
|
|
927
|
+
var t = L2 < 1e-12 ? 0 : Math.max(0, Math.min(1, ((px-a[0])*vx + (py-a[1])*vy) / L2));
|
|
928
|
+
best = Math.min(best, Math.hypot(px - a[0] - t*vx, py - a[1] - t*vy));
|
|
929
|
+
}
|
|
930
|
+
return best;
|
|
931
|
+
}
|
|
932
|
+
function refreshMarkers() {
|
|
933
|
+
// Badges removed (edge coloring carries R/B polarity) — empty implementation keeps the interface.
|
|
934
|
+
}
|
|
935
|
+
var drag = null, rafPending = false;
|
|
936
|
+
// ── v0.7.7 per-edge style cycle: auto -> straight -> arc -> free -> auto ──
|
|
937
|
+
var STYLE_ORDER = ['auto', 'straight', 'arc', 'free'];
|
|
938
|
+
function setStyle(i, st) {
|
|
939
|
+
if (st === 'auto') {
|
|
940
|
+
delete styles[i];
|
|
941
|
+
delete freeWps[i];
|
|
942
|
+
delete pinned[i]; // back to the engine default state (no pins)
|
|
943
|
+
} else {
|
|
944
|
+
styles[i] = st;
|
|
945
|
+
}
|
|
946
|
+
if (st === 'free' && !freeWps[i]) freeWps[i] = [];
|
|
947
|
+
safeRerouteUpdate(i, 'style change');
|
|
948
|
+
rebuildHandles();
|
|
949
|
+
refreshMetrics();
|
|
950
|
+
fireLayoutChanged();
|
|
951
|
+
}
|
|
952
|
+
function cycleStyle(i) {
|
|
953
|
+
var cur = styles[i] || 'auto';
|
|
954
|
+
setStyle(i, STYLE_ORDER[(STYLE_ORDER.indexOf(cur) + 1) % STYLE_ORDER.length]);
|
|
955
|
+
}
|
|
956
|
+
// manual double-click detection on edge pointerdowns: a fold-point insert
|
|
957
|
+
// puts a handle circle exactly under the cursor, so the second click of a
|
|
958
|
+
// browser dblclick lands on a different target and the native dblclick
|
|
959
|
+
// never fires. Track edge pointerdown timestamps instead (deterministic).
|
|
960
|
+
var lastEdgeDown = {edge: -1, t: 0};
|
|
961
|
+
function edgeDblClickCheck(i) {
|
|
962
|
+
var now = (window.performance && performance.now) ? performance.now() : Date.now();
|
|
963
|
+
if (lastEdgeDown.edge === i && now - lastEdgeDown.t < 450) {
|
|
964
|
+
lastEdgeDown = {edge: -1, t: 0};
|
|
965
|
+
cycleStyle(i);
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
lastEdgeDown = {edge: i, t: now};
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
// ── v0.7.7 free-mode fold points: click inserts, max 3 per edge ──
|
|
972
|
+
function projectOnSeg(p, a, b) {
|
|
973
|
+
var vx = b[0] - a[0], vy = b[1] - a[1], L2 = vx * vx + vy * vy;
|
|
974
|
+
var t = L2 < 1e-12 ? 0 : Math.max(0, Math.min(1, ((p[0] - a[0]) * vx + (p[1] - a[1]) * vy) / L2));
|
|
975
|
+
return [a[0] + t * vx, a[1] + t * vy];
|
|
976
|
+
}
|
|
977
|
+
var tipEl = null, tipTimer = null;
|
|
978
|
+
function showTip(text, ev) {
|
|
979
|
+
if (tipEl) tipEl.remove();
|
|
980
|
+
tipEl = document.createElement('div');
|
|
981
|
+
tipEl.className = 'stdraw-tip';
|
|
982
|
+
tipEl.textContent = text;
|
|
983
|
+
document.body.appendChild(tipEl);
|
|
984
|
+
var x = 40, y = 40;
|
|
985
|
+
if (ev && typeof ev.clientX === 'number') { x = ev.clientX + 12; y = ev.clientY - 24; }
|
|
986
|
+
tipEl.style.left = x + 'px';
|
|
987
|
+
tipEl.style.top = y + 'px';
|
|
988
|
+
if (tipTimer) clearTimeout(tipTimer);
|
|
989
|
+
tipTimer = setTimeout(function () { if (tipEl) tipEl.remove(); tipEl = null; }, 1500);
|
|
990
|
+
}
|
|
991
|
+
function selectNearestFreeHandle(i, p) {
|
|
992
|
+
var bestH = null, bestD = null;
|
|
993
|
+
handleLayer.querySelectorAll('.stdraw-wp[data-free="1"]').forEach(function (c) {
|
|
994
|
+
if (+c.getAttribute('data-edge') !== i) { c.classList.remove('sel'); return; }
|
|
995
|
+
var d = Math.hypot(+c.getAttribute('cx') - p[0], +c.getAttribute('cy') - p[1]);
|
|
996
|
+
if (bestD === null || d < bestD) { bestD = d; bestH = c; }
|
|
997
|
+
});
|
|
998
|
+
if (bestH) bestH.classList.add('sel');
|
|
999
|
+
}
|
|
1000
|
+
function insertWaypointAt(i, p) {
|
|
1001
|
+
// Shared fold-point creation (free click-insert + floating-bendpoint
|
|
1002
|
+
// conversion): projected onto the nearest segment, rounded to 0.5.
|
|
1003
|
+
// Returns the inserted freeWps index, or null at the MAX_FREE_PTS cap.
|
|
1004
|
+
var wps = freeWps[i] = freeWps[i] || [];
|
|
1005
|
+
if (wps.length >= MAX_FREE_PTS) return null;
|
|
1006
|
+
var pts = edges[i].pts;
|
|
1007
|
+
var best = null;
|
|
1008
|
+
for (var s = 0; s < pts.length - 1; s++) {
|
|
1009
|
+
var q = projectOnSeg(p, pts[s], pts[s + 1]);
|
|
1010
|
+
var d = Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
1011
|
+
if (best === null || d < best[0]) best = [d, s, q];
|
|
1012
|
+
}
|
|
1013
|
+
var wi = Math.min(best[1], wps.length);
|
|
1014
|
+
wps.splice(wi, 0, [Math.round(best[2][0] * 2) / 2, Math.round(best[2][1] * 2) / 2]);
|
|
1015
|
+
return wi;
|
|
1016
|
+
}
|
|
1017
|
+
function insertFreePoint(i, p, ev) {
|
|
1018
|
+
var wi = insertWaypointAt(i, p);
|
|
1019
|
+
if (wi === null) {
|
|
1020
|
+
selectNearestFreeHandle(i, p);
|
|
1021
|
+
showTip('max ' + MAX_FREE_PTS + ' points per edge', ev);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
reanchorFree(i);
|
|
1025
|
+
updateEdge(i);
|
|
1026
|
+
rebuildHandles();
|
|
1027
|
+
refreshMetrics();
|
|
1028
|
+
fireLayoutChanged();
|
|
1029
|
+
}
|
|
1030
|
+
// ── segment drag + floating bendpoints (drawio/bpmn-io caliber) ──
|
|
1031
|
+
var MERGE_TOL = 6; // svg-unit merge tolerance (fold point dropped back onto the line)
|
|
1032
|
+
function segDraggable(i) {
|
|
1033
|
+
// Segment drag applies only to edges with explicit user geometry: free
|
|
1034
|
+
// mode with fold points, or user-anchored pinned polylines. Straight
|
|
1035
|
+
// lines and auto-layout edges keep live re-routing; arcs have no
|
|
1036
|
+
// polyline segments.
|
|
1037
|
+
if (edges[i].arc) return false;
|
|
1038
|
+
if ((styles[i] || 'auto') === 'free') return !!(freeWps[i] && freeWps[i].length);
|
|
1039
|
+
return !!pinned[i];
|
|
1040
|
+
}
|
|
1041
|
+
function segAt(pts, p) {
|
|
1042
|
+
// svgPoint() returns an SVGPoint object (.x/.y), array callers pass
|
|
1043
|
+
// [x, y] — normalize to [x, y] here so every caller is safe
|
|
1044
|
+
if (p && typeof p.x === 'number') p = [p.x, p.y];
|
|
1045
|
+
var best = null;
|
|
1046
|
+
for (var s = 0; s < pts.length - 1; s++) {
|
|
1047
|
+
var q = projectOnSeg(p, pts[s], pts[s + 1]);
|
|
1048
|
+
var d = Math.hypot(q[0] - p[0], q[1] - p[1]);
|
|
1049
|
+
if (best === null || d < best[0]) best = [d, s, q];
|
|
1050
|
+
}
|
|
1051
|
+
return best === null ? null : {k: best[1], proj: best[2], dist: best[0]};
|
|
1052
|
+
}
|
|
1053
|
+
function orientCursor(pts, k) {
|
|
1054
|
+
// diagram-js segment-dragger convention: horizontal segments move
|
|
1055
|
+
// vertically (ns-resize), vertical segments move horizontally
|
|
1056
|
+
// (ew-resize).
|
|
1057
|
+
var a = pts[k], b = pts[k + 1];
|
|
1058
|
+
return Math.abs(b[1] - a[1]) <= Math.abs(b[0] - a[0]) ? 'ns-resize' : 'ew-resize';
|
|
1059
|
+
}
|
|
1060
|
+
function segDragPlan(i, k) {
|
|
1061
|
+
// drawio segment-move semantics: the dragged segment translates; jog
|
|
1062
|
+
// bendpoints are created at its pre-drag endpoints. Segments adjacent to
|
|
1063
|
+
// an edge anchor insert ONE bendpoint (the anchor side re-anchors on the
|
|
1064
|
+
// box border instead — same caliber as CLI --from-layout), interior
|
|
1065
|
+
// segments insert TWO. head/tail = user waypoints that stay put;
|
|
1066
|
+
// mv = the moving points (copies of the dragged segment's waypoint
|
|
1067
|
+
// endpoints); added = bendpoints created (mv.length).
|
|
1068
|
+
var e = edges[i], pts = e.pts;
|
|
1069
|
+
var isFree = (styles[i] || 'auto') === 'free';
|
|
1070
|
+
var W = isFree ? (freeWps[i] || []) : pts.slice(1, -1);
|
|
1071
|
+
var head, tail, mv;
|
|
1072
|
+
if (k === 0) { head = []; tail = W.map(cp); mv = [cp(pts[1])]; }
|
|
1073
|
+
else if (k === pts.length - 2) { head = W.map(cp); tail = []; mv = [cp(pts[k])]; }
|
|
1074
|
+
else {
|
|
1075
|
+
head = W.slice(0, k).map(cp); tail = W.slice(k).map(cp);
|
|
1076
|
+
mv = [cp(pts[k]), cp(pts[k + 1])];
|
|
1077
|
+
}
|
|
1078
|
+
return {head: head, tail: tail, mv: mv, added: mv.length};
|
|
1079
|
+
}
|
|
1080
|
+
function applyUserWps(i, wps) {
|
|
1081
|
+
// Write the effective user waypoints back: free mode via freeWps +
|
|
1082
|
+
// reanchorFree; pinned via the same anchor-first/last caliber.
|
|
1083
|
+
var e = edges[i];
|
|
1084
|
+
if ((styles[i] || 'auto') === 'free') { freeWps[i] = wps; reanchorFree(i); return; }
|
|
1085
|
+
if (!wps.length) {
|
|
1086
|
+
var A = nodes[e.a], B = nodes[e.b];
|
|
1087
|
+
e.pts = [endPt(i, 0, anchor(A, [B.x, B.y])), endPt(i, 1, anchor(B, [A.x, A.y]))];
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
e.pts = [endPt(i, 0, anchor(nodes[e.a], wps[0]))].concat(wps.map(cp))
|
|
1091
|
+
.concat([endPt(i, 1, anchor(nodes[e.b], wps[wps.length - 1]))]);
|
|
1092
|
+
}
|
|
1093
|
+
function effectiveWps(i) {
|
|
1094
|
+
if ((styles[i] || 'auto') === 'free') return (freeWps[i] || []).map(cp);
|
|
1095
|
+
return pinned[i] ? edges[i].pts.slice(1, -1).map(cp) : [];
|
|
1096
|
+
}
|
|
1097
|
+
function adoptFreeAndInsert(i, pos) {
|
|
1098
|
+
// Floating-bendpoint drag-away: adopt the edge's current geometry as
|
|
1099
|
+
// user fold points (free mode) and insert exactly one fold point at the
|
|
1100
|
+
// grab position. Returns the inserted freeWps index, or false at the cap
|
|
1101
|
+
// (in which case NOTHING is written — preview stays dirty-free).
|
|
1102
|
+
var e = edges[i], st = styles[i] || 'auto';
|
|
1103
|
+
var cur = (st === 'free') ? (freeWps[i] || [])
|
|
1104
|
+
: (pinned[i] ? e.pts.slice(1, -1)
|
|
1105
|
+
: (e.corners && e.corners.length >= 3 ? e.corners.slice(1, -1) : []));
|
|
1106
|
+
if (cur.length + 1 > MAX_FREE_PTS) return false;
|
|
1107
|
+
if (st !== 'free') {
|
|
1108
|
+
styles[i] = 'free';
|
|
1109
|
+
delete pinned[i];
|
|
1110
|
+
freeWps[i] = cur.map(function (q) {
|
|
1111
|
+
return [Math.round(q[0] * 2) / 2, Math.round(q[1] * 2) / 2];
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
var wi = insertWaypointAt(i, pos);
|
|
1115
|
+
if (wi === null) return false;
|
|
1116
|
+
reanchorFree(i);
|
|
1117
|
+
return wi;
|
|
1118
|
+
}
|
|
1119
|
+
function removeWaypoint(i, idx) {
|
|
1120
|
+
// Merge-back deletion: drop the fold point, then rebuild. Pinned edges
|
|
1121
|
+
// whose interior empties fall back to auto (no empty-pin residue).
|
|
1122
|
+
var e = edges[i];
|
|
1123
|
+
if ((styles[i] || 'auto') === 'free') {
|
|
1124
|
+
if (freeWps[i]) freeWps[i].splice(idx - 1, 1);
|
|
1125
|
+
reanchorFree(i);
|
|
1126
|
+
} else {
|
|
1127
|
+
e.pts.splice(idx, 1);
|
|
1128
|
+
if (e.pts.length <= 2) { delete pinned[i]; reroute(i); }
|
|
1129
|
+
}
|
|
1130
|
+
updateEdge(i);
|
|
1131
|
+
rebuildHandles();
|
|
1132
|
+
refreshMetrics();
|
|
1133
|
+
}
|
|
1134
|
+
// Floating bendpoint handle: a single preview element in its own layer
|
|
1135
|
+
// (rebuildHandles clears #stdraw-handles; the floating preview must
|
|
1136
|
+
// survive every rebuild). Hover-only — never written into user state.
|
|
1137
|
+
var floatLayer = document.createElementNS(NS, 'g');
|
|
1138
|
+
floatLayer.id = 'stdraw-float';
|
|
1139
|
+
svg.appendChild(floatLayer);
|
|
1140
|
+
var floatC = null;
|
|
1141
|
+
// Last cursor client position seen over an edge (drives the deterministic
|
|
1142
|
+
// floating-preview visibility check below)
|
|
1143
|
+
var lastFloatPos = null, lastCursor = null;
|
|
1144
|
+
function edgeFloatable(i) { return !edges[i].arc; }
|
|
1145
|
+
function floatHandle() {
|
|
1146
|
+
if (floatC) return floatC;
|
|
1147
|
+
floatC = document.createElementNS(NS, 'circle');
|
|
1148
|
+
floatC.setAttribute('r', '4');
|
|
1149
|
+
floatC.setAttribute('class', 'stdraw-wp floating');
|
|
1150
|
+
floatC.setAttribute('data-float', '1');
|
|
1151
|
+
floatC.style.display = 'none';
|
|
1152
|
+
floatLayer.appendChild(floatC);
|
|
1153
|
+
return floatC;
|
|
1154
|
+
}
|
|
1155
|
+
// Deterministic floating-preview lifecycle: boundary events (mouseenter/
|
|
1156
|
+
// mouseleave between the line and the handle) fire in a timing-dependent
|
|
1157
|
+
// order when the handle appears directly under a stationary cursor, so
|
|
1158
|
+
// visibility is decided by polling elementFromPoint at the last cursor
|
|
1159
|
+
// position — over the edge or the handle keeps the preview, anywhere else
|
|
1160
|
+
// hides it. Pure DOM geometry, no timers-on-events races.
|
|
1161
|
+
window.addEventListener('mousemove', function (ev) { lastCursor = [ev.clientX, ev.clientY]; });
|
|
1162
|
+
setInterval(function () {
|
|
1163
|
+
if (!floatC || floatC.style.display === 'none' || !lastCursor) return;
|
|
1164
|
+
// Probe the CURRENT cursor position (window-tracked; the parked handle
|
|
1165
|
+
// position would self-lock). Exclude the handle itself from the probe
|
|
1166
|
+
// via pointer-events:none — no visual flicker.
|
|
1167
|
+
var pe = floatC.style.pointerEvents;
|
|
1168
|
+
floatC.style.pointerEvents = 'none';
|
|
1169
|
+
var el = document.elementFromPoint(lastCursor[0], lastCursor[1]);
|
|
1170
|
+
floatC.style.pointerEvents = pe;
|
|
1171
|
+
if (el && el.closest && el.closest('[data-edge]')) return;
|
|
1172
|
+
hideFloat();
|
|
1173
|
+
}, 120);
|
|
1174
|
+
function showFloat(i, p) {
|
|
1175
|
+
if (drag || !edgeFloatable(i)) { hideFloat(); return; }
|
|
1176
|
+
var sd = segAt(edges[i].pts, p);
|
|
1177
|
+
if (!sd) { hideFloat(); return; }
|
|
1178
|
+
// bpmn-io .djs-bendpoint.floating parks the handle at the MIDDLE of the
|
|
1179
|
+
// hovered segment, NOT under the cursor: a cursor-following handle would
|
|
1180
|
+
// intercept every pointerdown on the line and make segment drag
|
|
1181
|
+
// unreachable. Mid-segment parking splits the intents cleanly — press at
|
|
1182
|
+
// mid = create fold point (drag away), press elsewhere = segment drag.
|
|
1183
|
+
var a = edges[i].pts[sd.k], b2 = edges[i].pts[sd.k + 1];
|
|
1184
|
+
var c = floatHandle();
|
|
1185
|
+
c.setAttribute('cx', ((a[0] + b2[0]) / 2).toFixed(1));
|
|
1186
|
+
c.setAttribute('cy', ((a[1] + b2[1]) / 2).toFixed(1));
|
|
1187
|
+
c.setAttribute('data-edge', i);
|
|
1188
|
+
c.style.display = '';
|
|
1189
|
+
}
|
|
1190
|
+
function hideFloat() {
|
|
1191
|
+
if (floatC) floatC.style.display = 'none';
|
|
1192
|
+
}
|
|
1193
|
+
// ══ t_2c2496ca: standard-anchor overlay + per-edge endpoint drag ══
|
|
1194
|
+
// Anchor dots: hover a box -> its 16 standard anchors appear (toggleable via
|
|
1195
|
+
// #stdraw-anchors-toggle; dots layer is pointer-events:none, click-to-assign
|
|
1196
|
+
// is resolved against the LIVE anchor coordinates in onDown). Endpoint drag:
|
|
1197
|
+
// every edge renders two endpoint handles in the topmost #stdraw-eps layer;
|
|
1198
|
+
// release snaps to the nearest standard anchor of the endpoint's own box and
|
|
1199
|
+
// records the explicit designation in userAnchored (re-projection caliber:
|
|
1200
|
+
// node drags afterwards only re-project the endpoint onto the SAME anchor).
|
|
1201
|
+
var anchorLayer = document.createElementNS(NS, 'g');
|
|
1202
|
+
anchorLayer.id = 'stdraw-anchors';
|
|
1203
|
+
svg.appendChild(anchorLayer);
|
|
1204
|
+
var anchorToggle = false, hoverNode = null; // t_51392037: anchor dots OFF by default (jacky: dots are visual noise; hover + toolbar toggle still show them)
|
|
1205
|
+
var ANCHOR_CLICK_TOL = 10; // px: a press within this distance of a visible
|
|
1206
|
+
// standard anchor is an explicit assignment
|
|
1207
|
+
function setAnchorToggle(on) {
|
|
1208
|
+
anchorToggle = !!on;
|
|
1209
|
+
var b = document.getElementById('stdraw-anchors-toggle');
|
|
1210
|
+
if (b) b.textContent = 'Anchors: ' + (anchorToggle ? 'on' : 'off');
|
|
1211
|
+
renderAnchors();
|
|
1212
|
+
}
|
|
1213
|
+
function renderAnchors() {
|
|
1214
|
+
while (anchorLayer.firstChild) anchorLayer.removeChild(anchorLayer.firstChild);
|
|
1215
|
+
if (!anchorToggle || !hoverNode || !nodes[hoverNode]) return;
|
|
1216
|
+
var pinnedAids = {};
|
|
1217
|
+
edges.forEach(function (e, i) {
|
|
1218
|
+
var ua = userAnchored[i];
|
|
1219
|
+
if (!ua) return;
|
|
1220
|
+
if (ua[0] && e.a === hoverNode) pinnedAids[ua[0]] = true;
|
|
1221
|
+
if (ua[1] && e.b === hoverNode) pinnedAids[ua[1]] = true;
|
|
1222
|
+
});
|
|
1223
|
+
stdAnchors(nodes[hoverNode]).forEach(function (p) {
|
|
1224
|
+
var c = document.createElementNS(NS, 'circle');
|
|
1225
|
+
c.setAttribute('cx', p[1].toFixed(1));
|
|
1226
|
+
c.setAttribute('cy', p[2].toFixed(1));
|
|
1227
|
+
c.setAttribute('r', pinnedAids[p[0]] ? '4.5' : '3');
|
|
1228
|
+
c.setAttribute('class', 'stdraw-anchor' + (pinnedAids[p[0]] ? ' pinned' : ''));
|
|
1229
|
+
c.setAttribute('data-node', hoverNode);
|
|
1230
|
+
c.setAttribute('data-aid', p[0]);
|
|
1231
|
+
anchorLayer.appendChild(c);
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
function markNearAnchor(v, x, y) {
|
|
1235
|
+
// live snap feedback while dragging an endpoint (works with the dots
|
|
1236
|
+
// hidden too: the ring is drawn unconditionally)
|
|
1237
|
+
renderAnchors();
|
|
1238
|
+
var aid = nearestAnchorId(v, x, y);
|
|
1239
|
+
if (!aid || !nodes[v]) return;
|
|
1240
|
+
stdAnchors(nodes[v]).forEach(function (p) {
|
|
1241
|
+
if (p[0] !== aid) return;
|
|
1242
|
+
var c = document.createElementNS(NS, 'circle');
|
|
1243
|
+
c.setAttribute('cx', p[1].toFixed(1));
|
|
1244
|
+
c.setAttribute('cy', p[2].toFixed(1));
|
|
1245
|
+
c.setAttribute('r', '6');
|
|
1246
|
+
c.setAttribute('class', 'stdraw-anchor near');
|
|
1247
|
+
c.setAttribute('data-node', v);
|
|
1248
|
+
c.setAttribute('data-aid', p[0]);
|
|
1249
|
+
anchorLayer.appendChild(c);
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
var epsLayer = document.createElementNS(NS, 'g');
|
|
1253
|
+
epsLayer.id = 'stdraw-eps';
|
|
1254
|
+
svg.appendChild(epsLayer); // appended LAST: endpoint hit priority over the
|
|
1255
|
+
// 12px segment hit band is structural (DOM order)
|
|
1256
|
+
var epEls = {}; // edgeIdx -> {0: [hit, vis], 1: [hit, vis]}
|
|
1257
|
+
function ensureEpEls(i) {
|
|
1258
|
+
if (epEls[i]) return;
|
|
1259
|
+
var rec = {};
|
|
1260
|
+
[0, 1].forEach(function (end) {
|
|
1261
|
+
[true, false].forEach(function (isHit) {
|
|
1262
|
+
var c = document.createElementNS(NS, 'circle');
|
|
1263
|
+
c.setAttribute('r', isHit ? '9' : '3.5');
|
|
1264
|
+
c.setAttribute('class', isHit ? 'stdraw-eps-hit' : 'stdraw-ep');
|
|
1265
|
+
c.setAttribute('data-edge', i);
|
|
1266
|
+
c.setAttribute('data-end', end);
|
|
1267
|
+
epsLayer.appendChild(c);
|
|
1268
|
+
rec[end] = rec[end] || [];
|
|
1269
|
+
rec[end].push(c);
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
epEls[i] = rec;
|
|
1273
|
+
}
|
|
1274
|
+
function updateEndpointHandles(i) {
|
|
1275
|
+
if (!edges[i]) return;
|
|
1276
|
+
ensureEpEls(i);
|
|
1277
|
+
var e = edges[i];
|
|
1278
|
+
if (e.pts.length < 2) return;
|
|
1279
|
+
[[0, e.pts[0]], [1, e.pts[e.pts.length - 1]]].forEach(function (pe) {
|
|
1280
|
+
epEls[i][pe[0]].forEach(function (c) {
|
|
1281
|
+
c.setAttribute('cx', pe[1][0].toFixed(1));
|
|
1282
|
+
c.setAttribute('cy', pe[1][1].toFixed(1));
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
function rebuildAllEndpointHandles() {
|
|
1287
|
+
edges.forEach(function (e, i) { updateEndpointHandles(i); });
|
|
1288
|
+
}
|
|
1289
|
+
edges.forEach(function (e, i) { ensureEpEls(i); });
|
|
1290
|
+
function arcEpPts(e, end, pt) {
|
|
1291
|
+
// endpoint moved on an arc: re-derive the sampled Bezier from the frozen
|
|
1292
|
+
// control state, translating ctrl in lockstep with the endpoint delta
|
|
1293
|
+
// (tangent-preserving; same caliber as arcFollow's mean translation)
|
|
1294
|
+
if (!(e.arc && e.ctrl0 && e.pa0 && e.pb0)) return null;
|
|
1295
|
+
var from = end === 0 ? e.pa0 : e.pb0;
|
|
1296
|
+
var other = end === 0 ? e.pb0 : e.pa0;
|
|
1297
|
+
var ctrl = [e.ctrl0[0] + (pt[0] - from[0]), e.ctrl0[1] + (pt[1] - from[1])];
|
|
1298
|
+
var pa = end === 0 ? pt : other;
|
|
1299
|
+
var pb = end === 0 ? other : pt;
|
|
1300
|
+
return quad(pa, ctrl, pb, 12);
|
|
1301
|
+
}
|
|
1302
|
+
function clampVB(x, y) {
|
|
1303
|
+
return [Math.max(4, Math.min(VB[2] - 4, x)), Math.max(4, Math.min(VB[3] - 4, y))];
|
|
1304
|
+
}
|
|
1305
|
+
function assignAnchor(v, aid, ax, ay) {
|
|
1306
|
+
// Explicit designation (anchor click): the clicked standard anchor goes to
|
|
1307
|
+
// the incident edge endpoint nearest to it (deterministic single winner;
|
|
1308
|
+
// only endpoints of box v are candidates).
|
|
1309
|
+
var best = null;
|
|
1310
|
+
edges.forEach(function (e, i) {
|
|
1311
|
+
if (e.a !== v && e.b !== v) return;
|
|
1312
|
+
var end = e.a === v ? 0 : 1;
|
|
1313
|
+
var p = e.pts[end === 0 ? 0 : e.pts.length - 1];
|
|
1314
|
+
var d = Math.hypot(p[0] - ax, p[1] - ay);
|
|
1315
|
+
if (best === null || d < best[0] - 1e-9) best = [d, i, end];
|
|
1316
|
+
});
|
|
1317
|
+
if (!best) { showTip('no edge attached to this box'); return; }
|
|
1318
|
+
var i2 = best[1], end2 = best[2];
|
|
1319
|
+
userAnchored[i2] = userAnchored[i2] || {};
|
|
1320
|
+
userAnchored[i2][end2] = aid;
|
|
1321
|
+
edges[i2].pts[end2 === 0 ? 0 : edges[i2].pts.length - 1] = [ax, ay];
|
|
1322
|
+
updateEdge(i2);
|
|
1323
|
+
updateEndpointHandles(i2);
|
|
1324
|
+
renderAnchors();
|
|
1325
|
+
refreshMetrics();
|
|
1326
|
+
fireLayoutChanged();
|
|
1327
|
+
}
|
|
1328
|
+
function svgPoint(ev) {
|
|
1329
|
+
var m = svg.getScreenCTM();
|
|
1330
|
+
if (!m) return null;
|
|
1331
|
+
var pt = svg.createSVGPoint();
|
|
1332
|
+
pt.x = ev.clientX; pt.y = ev.clientY;
|
|
1333
|
+
return pt.matrixTransform(m.inverse());
|
|
1334
|
+
}
|
|
1335
|
+
function onDown(ev) {
|
|
1336
|
+
var t = ev.target;
|
|
1337
|
+
// ── t_2c2496ca endpoint drag: the topmost #stdraw-eps hit circles claim
|
|
1338
|
+
// the press BEFORE the edge hit band (drag-line = segment translation) and
|
|
1339
|
+
// before the floating preview. Hit priority is structural + asserted by
|
|
1340
|
+
// the probe (drag state keys contain 'ep').
|
|
1341
|
+
var he = t && t.closest ? t.closest('.stdraw-eps-hit, .stdraw-ep') : null;
|
|
1342
|
+
if (he) {
|
|
1343
|
+
var eiE = +he.getAttribute('data-edge'), endE = +he.getAttribute('data-end');
|
|
1344
|
+
if (edges[eiE]) {
|
|
1345
|
+
var peE = svgPoint(ev);
|
|
1346
|
+
if (peE) {
|
|
1347
|
+
drag = {ep: true, edge: eiE, end: endE, start: [peE.x, peE.y], ev: ev};
|
|
1348
|
+
setEpVis(eiE, true); // t_51392037: show the dragged endpoint's handles
|
|
1349
|
+
try { he.setPointerCapture(ev.pointerId); } catch (eE0) {}
|
|
1350
|
+
ev.preventDefault();
|
|
1351
|
+
ev.stopPropagation();
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
// ── t_2c2496ca anchor click-to-assign: while a box's anchors are shown, a
|
|
1357
|
+
// press within ANCHOR_CLICK_TOL of one of its standard anchors explicitly
|
|
1358
|
+
// assigns that anchor (nearest incident endpoint wins, deterministic).
|
|
1359
|
+
if (hoverNode && anchorToggle && nodes[hoverNode]) {
|
|
1360
|
+
var pa2 = svgPoint(ev);
|
|
1361
|
+
if (pa2) {
|
|
1362
|
+
var apts = stdAnchors(nodes[hoverNode]), hitA = null;
|
|
1363
|
+
for (var ka = 0; ka < apts.length; ka++) {
|
|
1364
|
+
var da = Math.hypot(apts[ka][1] - pa2.x, apts[ka][2] - pa2.y);
|
|
1365
|
+
if (da <= ANCHOR_CLICK_TOL) { hitA = apts[ka]; break; }
|
|
1366
|
+
}
|
|
1367
|
+
if (hitA) {
|
|
1368
|
+
assignAnchor(hoverNode, hitA[0], hitA[1], hitA[2]);
|
|
1369
|
+
ev.preventDefault();
|
|
1370
|
+
ev.stopPropagation();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// Floating bendpoint: preview-only until dragged >=4px (conversion in
|
|
1376
|
+
// onMove); a plain click writes nothing.
|
|
1377
|
+
var hf = t && t.closest ? t.closest('.stdraw-wp[data-float="1"]') : null;
|
|
1378
|
+
if (hf) {
|
|
1379
|
+
var fEi = +hf.getAttribute('data-edge');
|
|
1380
|
+
if (!edges[fEi]) return;
|
|
1381
|
+
// The floating handle sits exactly under the cursor (it follows the
|
|
1382
|
+
// hover point), so a double-click whose second press lands on the edge
|
|
1383
|
+
// resolves here — it must participate in the manual dblclick detection
|
|
1384
|
+
// or the style cycle dies whenever the preview is visible (same
|
|
1385
|
+
// caliber as the free-handle branch below).
|
|
1386
|
+
if (edgeDblClickCheck(fEi)) {
|
|
1387
|
+
ev.preventDefault();
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
var fp = svgPoint(ev);
|
|
1391
|
+
if (!fp) return;
|
|
1392
|
+
if ((styles[fEi] || 'auto') === 'free') {
|
|
1393
|
+
// Legacy free-mode click-insert: the parked preview must NOT swallow
|
|
1394
|
+
// it. On free edges the press delegates to the standard insert flow
|
|
1395
|
+
// (click = insert, drag = segment move); floating adoption stays
|
|
1396
|
+
// exclusive to edges without explicit user geometry.
|
|
1397
|
+
drag = {insert: true, edge: fEi, start: [fp.x, fp.y], ev: ev};
|
|
1398
|
+
if (freeWps[fEi] && freeWps[fEi].length) {
|
|
1399
|
+
var sdF = segAt(edges[fEi].pts, fp);
|
|
1400
|
+
if (sdF) drag.segPrep = sdF.k;
|
|
1401
|
+
}
|
|
1402
|
+
hideFloat();
|
|
1403
|
+
ev.preventDefault();
|
|
1404
|
+
ev.stopPropagation();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
var fsd = segAt(edges[fEi].pts, fp);
|
|
1408
|
+
drag = {floatDrag: true, edge: fEi, start: [fp.x, fp.y],
|
|
1409
|
+
proj: fsd ? fsd.proj : [fp.x, fp.y], ev: ev};
|
|
1410
|
+
try { hf.setPointerCapture(ev.pointerId); } catch (e0) {}
|
|
1411
|
+
hideFloat();
|
|
1412
|
+
ev.preventDefault();
|
|
1413
|
+
ev.stopPropagation();
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
var h = t && t.closest ? t.closest('.stdraw-wp') : null;
|
|
1417
|
+
if (h) {
|
|
1418
|
+
var ei = +h.getAttribute('data-edge'), wi = +h.getAttribute('data-wp');
|
|
1419
|
+
if (!edges[ei]) return;
|
|
1420
|
+
var isFree = h.getAttribute('data-free') === '1';
|
|
1421
|
+
if (isFree && edgeDblClickCheck(ei)) {
|
|
1422
|
+
ev.preventDefault();
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (!isFree) {
|
|
1426
|
+
pinEdge(ei);
|
|
1427
|
+
updateEdge(ei);
|
|
1428
|
+
}
|
|
1429
|
+
var p0 = svgPoint(ev);
|
|
1430
|
+
drag = {wp: true, edge: ei, idx: wi, free: isFree, start: [p0.x, p0.y],
|
|
1431
|
+
ox: edges[ei].pts[wi][0], oy: edges[ei].pts[wi][1], g: h};
|
|
1432
|
+
try { h.setPointerCapture(ev.pointerId); } catch (e) {}
|
|
1433
|
+
ev.preventDefault();
|
|
1434
|
+
ev.stopPropagation();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
var g = t && t.closest ? t.closest('[data-node]') : null;
|
|
1438
|
+
if (g) {
|
|
1439
|
+
var id = g.getAttribute('data-node');
|
|
1440
|
+
var p = svgPoint(ev);
|
|
1441
|
+
if (!p || !(id in nodes)) return;
|
|
1442
|
+
drag = {id: id, g: g, start: [p.x, p.y], ox: nodes[id].x, oy: nodes[id].y};
|
|
1443
|
+
try { g.setPointerCapture(ev.pointerId); } catch (e) {}
|
|
1444
|
+
g.classList.add('dragging');
|
|
1445
|
+
ev.preventDefault();
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
// free-mode fold-point insertion + double-click style cycle: any
|
|
1449
|
+
// pointerdown resolving to an edge (hit path / visible line / free
|
|
1450
|
+
// handle) participates in the double-click detection
|
|
1451
|
+
var eg = t && t.closest ? t.closest('[data-edge]') : null;
|
|
1452
|
+
if (eg) {
|
|
1453
|
+
var ei2 = +eg.getAttribute('data-edge');
|
|
1454
|
+
if (edges[ei2]) {
|
|
1455
|
+
if (edgeDblClickCheck(ei2)) {
|
|
1456
|
+
ev.preventDefault();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if ((styles[ei2] || 'auto') === 'free') {
|
|
1460
|
+
var p1 = svgPoint(ev);
|
|
1461
|
+
if (p1) {
|
|
1462
|
+
drag = {insert: true, edge: ei2, start: [p1.x, p1.y], ev: ev};
|
|
1463
|
+
// explicit geometry: arm the segment-drag conversion (first
|
|
1464
|
+
// movement >=4px turns this click-insert into a segment drag)
|
|
1465
|
+
if (freeWps[ei2] && freeWps[ei2].length) {
|
|
1466
|
+
var sd0 = segAt(edges[ei2].pts, p1);
|
|
1467
|
+
if (sd0) drag.segPrep = sd0.k;
|
|
1468
|
+
}
|
|
1469
|
+
ev.preventDefault();
|
|
1470
|
+
}
|
|
1471
|
+
} else if (segDraggable(ei2)) {
|
|
1472
|
+
// pinned (user-anchored) polyline: pointerdown arms a segment drag;
|
|
1473
|
+
// a click without movement writes nothing
|
|
1474
|
+
var p2 = svgPoint(ev);
|
|
1475
|
+
if (p2) {
|
|
1476
|
+
var sd1 = segAt(edges[ei2].pts, p2);
|
|
1477
|
+
if (sd1) {
|
|
1478
|
+
drag = {seg: true, edge: ei2, plan: segDragPlan(ei2, sd1.k),
|
|
1479
|
+
start: [p2.x, p2.y], ev: ev};
|
|
1480
|
+
ev.preventDefault();
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function onMove(ev) {
|
|
1488
|
+
if (!drag) return;
|
|
1489
|
+
var p = svgPoint(ev);
|
|
1490
|
+
if (!p) return;
|
|
1491
|
+
if (drag.ep) {
|
|
1492
|
+
// t_2c2496ca endpoint drag: the endpoint follows the pointer (clamped to
|
|
1493
|
+
// the viewBox); the nearest standard anchor of the endpoint's own box is
|
|
1494
|
+
// highlighted live; the snap itself happens on release.
|
|
1495
|
+
var eE = edges[drag.edge];
|
|
1496
|
+
var ptE = clampVB(p.x, p.y);
|
|
1497
|
+
var arcPts = arcEpPts(eE, drag.end, ptE);
|
|
1498
|
+
if (arcPts) {
|
|
1499
|
+
eE.pts = arcPts;
|
|
1500
|
+
} else {
|
|
1501
|
+
eE.pts[drag.end === 0 ? 0 : eE.pts.length - 1] = ptE;
|
|
1502
|
+
}
|
|
1503
|
+
markNearAnchor(drag.end === 0 ? eE.a : eE.b, ptE[0], ptE[1]);
|
|
1504
|
+
updateEdge(drag.edge);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (drag.floatDrag) {
|
|
1508
|
+
// floating bendpoint: preview until the pointer moves >=4px, then
|
|
1509
|
+
// adopt the edge geometry + create exactly one fold point and hand
|
|
1510
|
+
// over to the standard fold-point drag; below the threshold = no-op
|
|
1511
|
+
if (!drag.converted) {
|
|
1512
|
+
var mf = Math.hypot(p.x - drag.start[0], p.y - drag.start[1]);
|
|
1513
|
+
if (mf < 4) return;
|
|
1514
|
+
var fi = drag.edge;
|
|
1515
|
+
var wi = adoptFreeAndInsert(fi, drag.proj);
|
|
1516
|
+
if (wi === false) {
|
|
1517
|
+
showTip('max ' + MAX_FREE_PTS + ' points per edge', drag.ev || ev);
|
|
1518
|
+
drag = null;
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
drag = {wp: true, free: true, edge: fi, idx: wi + 1, start: drag.start,
|
|
1522
|
+
ox: freeWps[fi][wi][0], oy: freeWps[fi][wi][1], g: null};
|
|
1523
|
+
rebuildHandles();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (drag.insert && drag.segPrep !== undefined) {
|
|
1528
|
+
// click-insert upgrades to a segment drag on first real movement
|
|
1529
|
+
var mi = Math.hypot(p.x - drag.start[0], p.y - drag.start[1]);
|
|
1530
|
+
if (mi >= 4) {
|
|
1531
|
+
var ci = drag.edge;
|
|
1532
|
+
var plan = segDragPlan(ci, drag.segPrep);
|
|
1533
|
+
if ((freeWps[ci] || []).length + plan.added > MAX_FREE_PTS) {
|
|
1534
|
+
showTip('max ' + MAX_FREE_PTS + ' points per edge', drag.ev || ev);
|
|
1535
|
+
drag = null;
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
drag = {seg: true, edge: ci, plan: plan, start: drag.start, ev: drag.ev};
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (drag.seg) {
|
|
1542
|
+
// whole-segment translation: static head/tail waypoints stay, the
|
|
1543
|
+
// moving copies of the dragged segment's endpoints follow the pointer
|
|
1544
|
+
var pl = drag.plan;
|
|
1545
|
+
var dx = p.x - drag.start[0], dy = p.y - drag.start[1];
|
|
1546
|
+
var mvPts = pl.mv.map(function (q) {
|
|
1547
|
+
return [Math.max(4, Math.min(VB[2] - 4, q[0] + dx)),
|
|
1548
|
+
Math.max(4, Math.min(VB[3] - 4, q[1] + dy))];
|
|
1549
|
+
});
|
|
1550
|
+
applyUserWps(drag.edge, pl.head.concat(mvPts, pl.tail));
|
|
1551
|
+
updateEdge(drag.edge);
|
|
1552
|
+
rebuildHandles();
|
|
1553
|
+
if (!rafPending) {
|
|
1554
|
+
rafPending = true;
|
|
1555
|
+
requestAnimationFrame(function () { rafPending = false; refreshMetrics(); });
|
|
1556
|
+
}
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (drag.wp) {
|
|
1560
|
+
var e = edges[drag.edge];
|
|
1561
|
+
var wx = drag.ox + (p.x - drag.start[0]);
|
|
1562
|
+
var wy = drag.oy + (p.y - drag.start[1]);
|
|
1563
|
+
wx = Math.max(4, Math.min(VB[2] - 4, wx));
|
|
1564
|
+
wy = Math.max(4, Math.min(VB[3] - 4, wy));
|
|
1565
|
+
e.pts[drag.idx] = [wx, wy];
|
|
1566
|
+
if (drag.free && freeWps[drag.edge]) freeWps[drag.edge][drag.idx - 1] = [wx, wy];
|
|
1567
|
+
updateEdge(drag.edge);
|
|
1568
|
+
rebuildHandles();
|
|
1569
|
+
if (!rafPending) {
|
|
1570
|
+
rafPending = true;
|
|
1571
|
+
requestAnimationFrame(function () { rafPending = false; refreshMetrics(); });
|
|
1572
|
+
}
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (drag.insert) {
|
|
1576
|
+
// Armed click-insert (free mode, fold points may be empty): movement is
|
|
1577
|
+
// neither an insert (that is decided on release, <4px) nor a drag —
|
|
1578
|
+
// fall-through here would hit the node branch with drag.id undefined
|
|
1579
|
+
// (latent TypeError, exposed by the segment-drag interaction paths).
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
var n = nodes[drag.id];
|
|
1583
|
+
var nx = drag.ox + (p.x - drag.start[0]);
|
|
1584
|
+
var ny = drag.oy + (p.y - drag.start[1]);
|
|
1585
|
+
nx = Math.max(n.w / 2 + 2, Math.min(VB[2] - n.w / 2 - 2, nx));
|
|
1586
|
+
ny = Math.max(nHH(n) + 2, Math.min(VB[3] - nHH(n) - 2, ny));
|
|
1587
|
+
n.x = nx; n.y = ny;
|
|
1588
|
+
drag.g.setAttribute('transform', 'translate(' + (nx - n.bx).toFixed(1) + ' ' + (ny - n.by).toFixed(1) + ')');
|
|
1589
|
+
var touched = {};
|
|
1590
|
+
incidentMap[drag.id].forEach(function (i) { touched[i] = 1; safeRerouteUpdate(i, 'drag re-anchor'); });
|
|
1591
|
+
renderAnchors(); // t_2c2496ca: anchor dots + pinned markers follow the moved box
|
|
1592
|
+
// Other edges: (1) pressed by the moving node (1px margin) -> re-route;
|
|
1593
|
+
// (2) currently polyline-shaped (detour / dense vg geometry) -> obstacles
|
|
1594
|
+
// changed so recompute (incl. stale detours invalidated when an obstacle is
|
|
1595
|
+
// dragged away). Preview caliber; user-anchored edges excepted (waypoints
|
|
1596
|
+
// preserved, endpoints re-anchored only).
|
|
1597
|
+
var hw = n.w / 2 + 1, hh = nHH(n) + 1;
|
|
1598
|
+
edges.forEach(function (e2, j) {
|
|
1599
|
+
if (touched[j] || pinned[j] || e2.a === drag.id || e2.b === drag.id) return;
|
|
1600
|
+
var need = e2.pts.length > 2;
|
|
1601
|
+
if (!need) {
|
|
1602
|
+
var s = samplePts(e2.pts, 6);
|
|
1603
|
+
for (var k = 0; k < s.length; k++) {
|
|
1604
|
+
if (distRect(s[k][0], s[k][1], nx, ny, hw, hh) <= 0) { need = true; break; }
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (need) { safeRerouteUpdate(j, 'drag reroute'); }
|
|
1608
|
+
});
|
|
1609
|
+
rebuildHandles();
|
|
1610
|
+
if (!rafPending) {
|
|
1611
|
+
rafPending = true;
|
|
1612
|
+
requestAnimationFrame(function () { rafPending = false; refreshMetrics(); });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function onUp(ev) {
|
|
1616
|
+
if (!drag) return;
|
|
1617
|
+
if (drag.ep) {
|
|
1618
|
+
// t_2c2496ca endpoint release: snap to the nearest standard anchor of the
|
|
1619
|
+
// endpoint's OWN box (deterministic tie -> lower ANCHOR_IDS index) and
|
|
1620
|
+
// record the explicit designation; subsequent node drags only re-project
|
|
1621
|
+
// this endpoint onto the SAME anchor position.
|
|
1622
|
+
var eiU = drag.edge, endU = drag.end;
|
|
1623
|
+
var eU = edges[eiU];
|
|
1624
|
+
var peU = svgPoint(ev);
|
|
1625
|
+
var vU = endU === 0 ? eU.a : eU.b;
|
|
1626
|
+
var xU, yU;
|
|
1627
|
+
if (peU) { var cU = clampVB(peU.x, peU.y); xU = cU[0]; yU = cU[1]; }
|
|
1628
|
+
else { var qU = eU.pts[endU === 0 ? 0 : eU.pts.length - 1]; xU = qU[0]; yU = qU[1]; }
|
|
1629
|
+
var aidU = nearestAnchorId(vU, xU, yU);
|
|
1630
|
+
var coordU = null;
|
|
1631
|
+
if (aidU) stdAnchors(nodes[vU]).forEach(function (q) {
|
|
1632
|
+
if (q[0] === aidU) coordU = [q[1], q[2]];
|
|
1633
|
+
});
|
|
1634
|
+
if (aidU && coordU) {
|
|
1635
|
+
userAnchored[eiU] = userAnchored[eiU] || {};
|
|
1636
|
+
userAnchored[eiU][endU] = aidU;
|
|
1637
|
+
var arcU = arcEpPts(eU, endU, coordU);
|
|
1638
|
+
if (arcU) eU.pts = arcU;
|
|
1639
|
+
else eU.pts[endU === 0 ? 0 : eU.pts.length - 1] = coordU;
|
|
1640
|
+
}
|
|
1641
|
+
drag = null;
|
|
1642
|
+
setEpVis(eiU, false); // t_51392037: drag over, dots hide again
|
|
1643
|
+
updateEdge(eiU);
|
|
1644
|
+
renderAnchors();
|
|
1645
|
+
refreshMetrics();
|
|
1646
|
+
fireLayoutChanged();
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
// floating bendpoint click (no drag): preview only, nothing persisted
|
|
1650
|
+
if (drag.floatDrag) { drag = null; hideFloat(); return; }
|
|
1651
|
+
if (drag.insert) {
|
|
1652
|
+
var pe = svgPoint(ev);
|
|
1653
|
+
var moved = pe ? Math.hypot(pe.x - drag.start[0], pe.y - drag.start[1]) : 1e9;
|
|
1654
|
+
if (moved < 4 && (styles[drag.edge] || 'auto') === 'free') {
|
|
1655
|
+
insertFreePoint(drag.edge, [pe.x, pe.y], ev);
|
|
1656
|
+
}
|
|
1657
|
+
drag = null;
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (drag.seg) {
|
|
1661
|
+
// finalize the segment drag: round the waypoints to the same 0.5
|
|
1662
|
+
// caliber as fold-point insertion, then persist via layoutchanged
|
|
1663
|
+
var se = drag;
|
|
1664
|
+
drag = null;
|
|
1665
|
+
var fw = effectiveWps(se.edge).map(function (q) {
|
|
1666
|
+
return [Math.round(q[0] * 2) / 2, Math.round(q[1] * 2) / 2];
|
|
1667
|
+
});
|
|
1668
|
+
applyUserWps(se.edge, fw);
|
|
1669
|
+
updateEdge(se.edge);
|
|
1670
|
+
rebuildHandles();
|
|
1671
|
+
refreshMarkers();
|
|
1672
|
+
refreshMetrics();
|
|
1673
|
+
fireLayoutChanged();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (drag.wp) {
|
|
1677
|
+
// merge-back deletion (finalized lifecycle semantics): a fold point
|
|
1678
|
+
// released within MERGE_TOL of the polyline through its neighbors is
|
|
1679
|
+
// removed instead of persisted (only after a real drag >=4px, so a
|
|
1680
|
+
// plain handle click never deletes)
|
|
1681
|
+
var eiU = drag.edge;
|
|
1682
|
+
var peU = svgPoint(ev);
|
|
1683
|
+
var movedU = peU ? Math.hypot(peU.x - drag.start[0], peU.y - drag.start[1]) : 1e9;
|
|
1684
|
+
var posU = drag.free ? (freeWps[eiU] || [])[drag.idx - 1]
|
|
1685
|
+
: (edges[eiU] ? edges[eiU].pts[drag.idx] : null);
|
|
1686
|
+
var merged = false;
|
|
1687
|
+
if (posU && movedU >= 4) {
|
|
1688
|
+
var restU = edges[eiU].pts.slice(0, drag.idx)
|
|
1689
|
+
.concat(edges[eiU].pts.slice(drag.idx + 1));
|
|
1690
|
+
if (ptPolyDist(posU[0], posU[1], restU) <= MERGE_TOL) {
|
|
1691
|
+
removeWaypoint(eiU, drag.idx);
|
|
1692
|
+
showTip('fold point merged', ev);
|
|
1693
|
+
merged = true;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
drag = null;
|
|
1697
|
+
if (merged) { refreshMarkers(); fireLayoutChanged(); return; }
|
|
1698
|
+
rebuildHandles();
|
|
1699
|
+
refreshMarkers();
|
|
1700
|
+
refreshMetrics();
|
|
1701
|
+
fireLayoutChanged();
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
drag.g.classList.remove('dragging');
|
|
1705
|
+
var id = drag.id;
|
|
1706
|
+
drag = null;
|
|
1707
|
+
incidentMap[id].forEach(function (i) { reroute(i); updateEdge(i); });
|
|
1708
|
+
renderAnchors(); // t_2c2496ca: pinned-anchor dot markers refresh on release
|
|
1709
|
+
rebuildHandles();
|
|
1710
|
+
refreshMarkers();
|
|
1711
|
+
refreshMetrics();
|
|
1712
|
+
fireLayoutChanged();
|
|
1713
|
+
}
|
|
1714
|
+
svg.addEventListener('pointerdown', onDown);
|
|
1715
|
+
window.addEventListener('pointermove', onMove);
|
|
1716
|
+
window.addEventListener('pointerup', onUp);
|
|
1717
|
+
window.addEventListener('pointercancel', onUp);
|
|
1718
|
+
function buildLayout() {
|
|
1719
|
+
var out = {tool: 'stdraw', kind: 'manual-layout', graph: D.name,
|
|
1720
|
+
viewBox: VB.slice(), nodes: {}};
|
|
1721
|
+
Object.keys(nodes).forEach(function (v) {
|
|
1722
|
+
out.nodes[v] = {x: Math.round(nodes[v].x), y: Math.round(nodes[v].y)};
|
|
1723
|
+
});
|
|
1724
|
+
// v0.7.7: edges[] carries the per-edge style + user fold points
|
|
1725
|
+
// (deterministic: original edge order, rounded coordinates); legacy
|
|
1726
|
+
// user-anchored (pinned) edges keep the waypoints-only form
|
|
1727
|
+
var es = [];
|
|
1728
|
+
edges.forEach(function (e, i) {
|
|
1729
|
+
var st = styles[i] || 'auto';
|
|
1730
|
+
var wps = null;
|
|
1731
|
+
if (st === 'free' && freeWps[i] && freeWps[i].length) {
|
|
1732
|
+
wps = freeWps[i].map(function (p) {
|
|
1733
|
+
return [Math.round(p[0]), Math.round(p[1])];
|
|
1734
|
+
});
|
|
1735
|
+
} else if (pinned[i]) {
|
|
1736
|
+
wps = e.pts.slice(1, -1).map(function (p) {
|
|
1737
|
+
return [Math.round(p[0]), Math.round(p[1])];
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
var ua = userAnchored[i];
|
|
1741
|
+
var hasUa = !!(ua && (ua[0] || ua[1]));
|
|
1742
|
+
if (st !== 'auto' || (wps && wps.length) || hasUa) {
|
|
1743
|
+
var it = {a: e.a, b: e.b, style: st};
|
|
1744
|
+
if (wps && wps.length) it.waypoints = wps;
|
|
1745
|
+
// t_2c2496ca: explicit standard-anchor designations travel with the
|
|
1746
|
+
// export (viewer round-trip via __stdrawApplyLayout edge_anchors)
|
|
1747
|
+
if (hasUa) {
|
|
1748
|
+
it.anchors = {};
|
|
1749
|
+
if (ua[0]) it.anchors.src = ua[0];
|
|
1750
|
+
if (ua[1]) it.anchors.dst = ua[1];
|
|
1751
|
+
}
|
|
1752
|
+
es.push(it);
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
if (es.length) out.edges = es;
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
function exportLayout() {
|
|
1759
|
+
var out = buildLayout();
|
|
1760
|
+
var txt = JSON.stringify(out, null, 1);
|
|
1761
|
+
try {
|
|
1762
|
+
var blob = new Blob([txt], {type: 'application/json'});
|
|
1763
|
+
var a = document.createElement('a');
|
|
1764
|
+
a.href = URL.createObjectURL(blob);
|
|
1765
|
+
a.download = (D.name || 'graph') + '-manual-layout.json';
|
|
1766
|
+
document.body.appendChild(a);
|
|
1767
|
+
a.click();
|
|
1768
|
+
document.body.removeChild(a);
|
|
1769
|
+
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
|
|
1770
|
+
} catch (e) { /* sandboxed / no download API: probes go through __stdrawExportJSON */ }
|
|
1771
|
+
return out;
|
|
1772
|
+
}
|
|
1773
|
+
// v0.7.7: layout-changed notification (drag release / fold-point insert /
|
|
1774
|
+
// style cycle / reset / applyLayout) — product integrations can listen on
|
|
1775
|
+
// window 'stdraw:layoutchanged' instead of polling pointerup
|
|
1776
|
+
function fireLayoutChanged() {
|
|
1777
|
+
try {
|
|
1778
|
+
window.dispatchEvent(new CustomEvent('stdraw:layoutchanged',
|
|
1779
|
+
{detail: buildLayout()}));
|
|
1780
|
+
} catch (e) { /* CustomEvent unavailable: silent */ }
|
|
1781
|
+
}
|
|
1782
|
+
function reset() {
|
|
1783
|
+
pinned = {};
|
|
1784
|
+
styles = {}; // v0.7.7: reset clears pins + forced styles -> auto
|
|
1785
|
+
freeWps = {};
|
|
1786
|
+
userAnchored = {}; // t_2c2496ca: reset also clears explicit anchor designations
|
|
1787
|
+
Object.keys(nodes).forEach(function (v) {
|
|
1788
|
+
var n = nodes[v];
|
|
1789
|
+
n.x = n.bx; n.y = n.by;
|
|
1790
|
+
if (n.g) n.g.removeAttribute('transform');
|
|
1791
|
+
});
|
|
1792
|
+
edges.forEach(function (e, i) {
|
|
1793
|
+
// v0.7.11 per-edge isolation on the restore path as well
|
|
1794
|
+
try {
|
|
1795
|
+
e.pts = e.base.map(cp);
|
|
1796
|
+
e.corners = e.via ? e.via.map(cp) : null;
|
|
1797
|
+
var g = visG(i);
|
|
1798
|
+
if (!g) return;
|
|
1799
|
+
var paths = g.querySelectorAll('path');
|
|
1800
|
+
// Original rendering (vg path_d incl. fillet arcs) restored from the initial d strings
|
|
1801
|
+
if (paths[0] && e.d0) paths[0].setAttribute('d', e.d0);
|
|
1802
|
+
else if (paths[0]) paths[0].setAttribute('d', edgeD(e.pts));
|
|
1803
|
+
if (paths[1]) {
|
|
1804
|
+
var n2 = e.pts.length, a = e.pts[n2 - 2], b = e.pts[n2 - 1];
|
|
1805
|
+
var ang = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
|
|
1806
|
+
paths[1].setAttribute('transform', 'translate(' + b[0].toFixed(1) + ' ' + b[1].toFixed(1) + ') rotate(' + ang.toFixed(1) + ')');
|
|
1807
|
+
}
|
|
1808
|
+
var h = hitP(i);
|
|
1809
|
+
if (h) h.setAttribute('d', edgeD(e.pts));
|
|
1810
|
+
clearDegraded(i);
|
|
1811
|
+
} catch (errR) { markDegraded(i, 'reset', errR); }
|
|
1812
|
+
});
|
|
1813
|
+
Object.keys(polGroups).forEach(function (i) {
|
|
1814
|
+
polGroups[i].g.setAttribute('transform', polGroups[i].t);
|
|
1815
|
+
});
|
|
1816
|
+
syncDomGeometry(); // v0.7.9: keep cards/anchors in sync after restore (no-op when in sync)
|
|
1817
|
+
rebuildHandles();
|
|
1818
|
+
rebuildAllEndpointHandles(); // t_2c2496ca
|
|
1819
|
+
renderAnchors(); // t_2c2496ca: clear stale dots / pinned markers
|
|
1820
|
+
refreshMetrics();
|
|
1821
|
+
fireLayoutChanged();
|
|
1822
|
+
}
|
|
1823
|
+
window.__stdrawExportLayout = exportLayout;
|
|
1824
|
+
window.__stdrawExportJSON = function () { return JSON.stringify(buildLayout(), null, 1); };
|
|
1825
|
+
// v0.7.7 public apply API: programmatically restore a layout
|
|
1826
|
+
// ({nodes: {id: {x,y}}, edge_styles: {"a->b": "straight"|"arc"|"free"|"auto"},
|
|
1827
|
+
// waypoints: {"a->b": [[x,y],...]}}) — the inverse of "Export layout"
|
|
1828
|
+
window.__stdrawApplyLayout = function (spec) {
|
|
1829
|
+
if (!spec || typeof spec !== 'object') return false;
|
|
1830
|
+
var ns = spec.nodes || {};
|
|
1831
|
+
Object.keys(ns).forEach(function (v) {
|
|
1832
|
+
if (!nodes[v]) return;
|
|
1833
|
+
var q = ns[v];
|
|
1834
|
+
nodes[v].x = +q.x;
|
|
1835
|
+
nodes[v].y = +q.y;
|
|
1836
|
+
});
|
|
1837
|
+
Object.keys(nodes).forEach(function (v) {
|
|
1838
|
+
var n = nodes[v];
|
|
1839
|
+
if (n.g) n.g.setAttribute('transform',
|
|
1840
|
+
'translate(' + (n.x - n.bx).toFixed(1) + ' ' + (n.y - n.by).toFixed(1) + ')');
|
|
1841
|
+
});
|
|
1842
|
+
function edgeIdxByKey(k) {
|
|
1843
|
+
var m = String(k).split('->');
|
|
1844
|
+
if (m.length !== 2) return null;
|
|
1845
|
+
for (var i = 0; i < edges.length; i++) {
|
|
1846
|
+
if (edges[i].a === m[0] && edges[i].b === m[1]) return i;
|
|
1847
|
+
}
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
styles = {};
|
|
1851
|
+
freeWps = {};
|
|
1852
|
+
userAnchored = {};
|
|
1853
|
+
var ess = spec.edge_styles || {};
|
|
1854
|
+
Object.keys(ess).forEach(function (k) {
|
|
1855
|
+
var i = edgeIdxByKey(k);
|
|
1856
|
+
if (i !== null && ['auto', 'straight', 'arc', 'free'].indexOf(ess[k]) >= 0) {
|
|
1857
|
+
if (ess[k] === 'free') freeWps[i] = [];
|
|
1858
|
+
if (ess[k] !== 'auto') styles[i] = ess[k];
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
var wss = spec.waypoints || {};
|
|
1862
|
+
Object.keys(wss).forEach(function (k) {
|
|
1863
|
+
var i = edgeIdxByKey(k);
|
|
1864
|
+
if (i === null || !Array.isArray(wss[k])) return;
|
|
1865
|
+
freeWps[i] = wss[k].map(function (p) { return [+p[0], +p[1]]; })
|
|
1866
|
+
.filter(function (p) { return isFinite(p[0]) && isFinite(p[1]); })
|
|
1867
|
+
.slice(0, MAX_FREE_PTS);
|
|
1868
|
+
if (!styles[i]) styles[i] = 'free';
|
|
1869
|
+
});
|
|
1870
|
+
// t_2c2496ca: restore explicit standard-anchor designations (applied before
|
|
1871
|
+
// the re-anchor round so the re-projection caliber sees them)
|
|
1872
|
+
var uas = spec.edge_anchors || {};
|
|
1873
|
+
Object.keys(uas).forEach(function (k) {
|
|
1874
|
+
var i = edgeIdxByKey(k);
|
|
1875
|
+
if (i === null || !uas[k] || typeof uas[k] !== 'object') return;
|
|
1876
|
+
var rec = {};
|
|
1877
|
+
if (ANCHOR_IDS.indexOf(uas[k].src) >= 0) rec[0] = uas[k].src;
|
|
1878
|
+
if (ANCHOR_IDS.indexOf(uas[k].dst) >= 0) rec[1] = uas[k].dst;
|
|
1879
|
+
if (rec[0] !== undefined || rec[1] !== undefined) userAnchored[i] = rec;
|
|
1880
|
+
});
|
|
1881
|
+
edges.forEach(function (e, i) { safeRerouteUpdate(i, 'applyLayout re-anchor'); });
|
|
1882
|
+
rebuildAllEndpointHandles();
|
|
1883
|
+
rebuildHandles();
|
|
1884
|
+
refreshMetrics();
|
|
1885
|
+
fireLayoutChanged();
|
|
1886
|
+
return true;
|
|
1887
|
+
};
|
|
1888
|
+
// v0.7.7 probe hooks: styled-edge list / per-edge style / free fold points
|
|
1889
|
+
window.__stdrawStyles = function () {
|
|
1890
|
+
return Object.keys(styles).map(Number).sort(function (a, b) { return a - b; });
|
|
1891
|
+
};
|
|
1892
|
+
window.__stdrawEdgeStyle = function (i) {
|
|
1893
|
+
return (i === undefined || i === null) ? JSON.parse(JSON.stringify(styles))
|
|
1894
|
+
: (styles[i] || 'auto');
|
|
1895
|
+
};
|
|
1896
|
+
window.__stdrawFreePoints = function (i) {
|
|
1897
|
+
return (i === undefined || i === null)
|
|
1898
|
+
? JSON.parse(JSON.stringify(freeWps))
|
|
1899
|
+
: (freeWps[i] ? freeWps[i].map(cp) : []);
|
|
1900
|
+
};
|
|
1901
|
+
window.__stdrawMetrics = metrics;
|
|
1902
|
+
window.__stdrawNodePos = function (v) {
|
|
1903
|
+
return v ? [nodes[v].x, nodes[v].y]
|
|
1904
|
+
: Object.keys(nodes).map(function (k) { return [k, nodes[k].x, nodes[k].y]; });
|
|
1905
|
+
};
|
|
1906
|
+
// Probe hooks: current polyline vertices / anchored state / vg edge list / handle list
|
|
1907
|
+
window.__stdrawEdgePts = function (i) {
|
|
1908
|
+
return (i === undefined || i === null)
|
|
1909
|
+
? edges.map(function (e) { return e.pts; })
|
|
1910
|
+
: (edges[i] ? edges[i].pts : null);
|
|
1911
|
+
};
|
|
1912
|
+
window.__stdrawPinned = function () {
|
|
1913
|
+
return Object.keys(pinned).map(Number);
|
|
1914
|
+
};
|
|
1915
|
+
window.__stdrawVgEdges = function () {
|
|
1916
|
+
var out = [];
|
|
1917
|
+
edges.forEach(function (e, i) { if (e.via) out.push(i); });
|
|
1918
|
+
return out;
|
|
1919
|
+
};
|
|
1920
|
+
window.__stdrawHandles = function () {
|
|
1921
|
+
var out = [];
|
|
1922
|
+
handleLayer.querySelectorAll('.stdraw-wp').forEach(function (c) {
|
|
1923
|
+
out.push([+c.getAttribute('data-edge'), +c.getAttribute('data-wp'),
|
|
1924
|
+
+c.getAttribute('cx'), +c.getAttribute('cy')]);
|
|
1925
|
+
});
|
|
1926
|
+
return out;
|
|
1927
|
+
};
|
|
1928
|
+
// Segment-drag / floating-bendpoint probe hooks: effective user waypoints
|
|
1929
|
+
// per edge (free or pinned interior) + floating preview handle state.
|
|
1930
|
+
window.__stdrawUserWps = function () {
|
|
1931
|
+
var out = {};
|
|
1932
|
+
edges.forEach(function (e, i) {
|
|
1933
|
+
var w = effectiveWps(i);
|
|
1934
|
+
if (w.length) out[i] = w;
|
|
1935
|
+
});
|
|
1936
|
+
return out;
|
|
1937
|
+
};
|
|
1938
|
+
window.__stdrawFloating = function () {
|
|
1939
|
+
if (!floatC || floatC.style.display === 'none') return null;
|
|
1940
|
+
return {edge: +floatC.getAttribute('data-edge'),
|
|
1941
|
+
cx: +floatC.getAttribute('cx'), cy: +floatC.getAttribute('cy')};
|
|
1942
|
+
};
|
|
1943
|
+
window.__stdrawSegDraggable = function (i) { return segDraggable(i); };
|
|
1944
|
+
// v0.7.11 fault-injection hook for the DOM-truth probe (t_3fb02650): force a
|
|
1945
|
+
// single edge's reroute to throw so the probe can assert per-edge isolation +
|
|
1946
|
+
// the visible degradation marker; pass false to clear (self-heal check).
|
|
1947
|
+
window.__stdrawProbeBreakEdge = function (i, on) {
|
|
1948
|
+
if (edges[i]) edges[i]._probeThrow = !!on;
|
|
1949
|
+
return true;
|
|
1950
|
+
};
|
|
1951
|
+
window.__stdrawDragState = function () {
|
|
1952
|
+
if (!drag) return null;
|
|
1953
|
+
var keys = [];
|
|
1954
|
+
for (var k in drag) keys.push(k);
|
|
1955
|
+
return {keys: keys, edge: drag.edge, free: drag.free};
|
|
1956
|
+
};
|
|
1957
|
+
window.__stdrawArcFlag = function (i) { return !!edges[i].arc; };
|
|
1958
|
+
// ── t_2c2496ca probe/integration hooks ──
|
|
1959
|
+
window.__stdrawAnchors = function (v) {
|
|
1960
|
+
return nodes[v] ? stdAnchors(nodes[v]) : null;
|
|
1961
|
+
};
|
|
1962
|
+
window.__stdrawUserAnchors = function () {
|
|
1963
|
+
return JSON.parse(JSON.stringify(userAnchored));
|
|
1964
|
+
};
|
|
1965
|
+
window.__stdrawSetAnchor = function (i, end, aid) {
|
|
1966
|
+
if (!edges[i] || ANCHOR_IDS.indexOf(aid) < 0 || (end !== 0 && end !== 1)) return false;
|
|
1967
|
+
userAnchored[i] = userAnchored[i] || {};
|
|
1968
|
+
userAnchored[i][end] = aid;
|
|
1969
|
+
safeRerouteUpdate(i, 'set anchor');
|
|
1970
|
+
updateEdge(i);
|
|
1971
|
+
renderAnchors();
|
|
1972
|
+
refreshMetrics();
|
|
1973
|
+
fireLayoutChanged();
|
|
1974
|
+
return true;
|
|
1975
|
+
};
|
|
1976
|
+
window.__stdrawAnchorDots = function () {
|
|
1977
|
+
var out = [];
|
|
1978
|
+
anchorLayer.querySelectorAll('circle.stdraw-anchor').forEach(function (c) {
|
|
1979
|
+
out.push([c.getAttribute('data-node'), c.getAttribute('data-aid'),
|
|
1980
|
+
+c.getAttribute('cx'), +c.getAttribute('cy')]);
|
|
1981
|
+
});
|
|
1982
|
+
return out;
|
|
1983
|
+
};
|
|
1984
|
+
window.__stdrawAnchorToggleState = function () { return !!anchorToggle; };
|
|
1985
|
+
window.__stdrawSetAnchorToggle = function (on) { setAnchorToggle(!!on); }; // t_51392037 probe hook
|
|
1986
|
+
window.__stdrawEndpointHandleVis = function () {
|
|
1987
|
+
var out = [];
|
|
1988
|
+
epsLayer.querySelectorAll('circle.stdraw-ep').forEach(function (c) {
|
|
1989
|
+
out.push([+c.getAttribute('data-edge'), +c.getAttribute('data-end'),
|
|
1990
|
+
getComputedStyle(c).opacity]);
|
|
1991
|
+
});
|
|
1992
|
+
return out;
|
|
1993
|
+
};
|
|
1994
|
+
window.__stdrawEndpointHandles = function () {
|
|
1995
|
+
var out = [];
|
|
1996
|
+
epsLayer.querySelectorAll('circle.stdraw-ep').forEach(function (c) {
|
|
1997
|
+
out.push([+c.getAttribute('data-edge'), +c.getAttribute('data-end'),
|
|
1998
|
+
+c.getAttribute('cx'), +c.getAttribute('cy')]);
|
|
1999
|
+
});
|
|
2000
|
+
return out;
|
|
2001
|
+
};
|
|
2002
|
+
// v0.7.9 probe/integration hooks: per-card rendered geometry + manual re-anchor
|
|
2003
|
+
window.__stdrawNodeGeom = function (v) {
|
|
2004
|
+
return v ? {x: nodes[v].x, y: nodes[v].y, w: nodes[v].w, h: nodes[v].h}
|
|
2005
|
+
: Object.keys(nodes).map(function (k) {
|
|
2006
|
+
return [k, nodes[k].x, nodes[k].y, nodes[k].w, nodes[k].h];
|
|
2007
|
+
});
|
|
2008
|
+
};
|
|
2009
|
+
window.__stdrawSyncDomGeometry = syncDomGeometry;
|
|
2010
|
+
var btnE = document.getElementById('stdraw-export');
|
|
2011
|
+
var btnR = document.getElementById('stdraw-reset');
|
|
2012
|
+
if (btnE) btnE.addEventListener('click', exportLayout);
|
|
2013
|
+
if (btnR) btnR.addEventListener('click', reset);
|
|
2014
|
+
var btnA = document.getElementById('stdraw-anchors-toggle');
|
|
2015
|
+
if (btnA) {
|
|
2016
|
+
btnA.textContent = 'Anchors: ' + (anchorToggle ? 'on' : 'off'); // t_51392037: default off
|
|
2017
|
+
btnA.addEventListener('click', function () { setAnchorToggle(!anchorToggle); });
|
|
2018
|
+
}
|
|
2019
|
+
rebuildHandles();
|
|
2020
|
+
rebuildAllEndpointHandles(); // t_2c2496ca: initial endpoint handle placement
|
|
2021
|
+
refreshMetrics();
|
|
2022
|
+
// v0.7.9 initial font-environment sync: run now (the script sits after the
|
|
2023
|
+
// SVG in the DOM, so layout is available) and again on window load (late
|
|
2024
|
+
// font/measure settling). Both calls are strict no-ops when the rendered
|
|
2025
|
+
// text fits the planned cards, so the deterministic initial render is
|
|
2026
|
+
// untouched in matching environments.
|
|
2027
|
+
syncDomGeometry();
|
|
2028
|
+
window.addEventListener('load', function () { syncDomGeometry(); });
|
|
2029
|
+
})();
|
|
2030
|
+
"""
|