dsh-model-everything 0.5.1 → 0.6.0

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.
package/lib/client.js CHANGED
@@ -29,9 +29,13 @@ window.__ModuleLoader__.load({
29
29
  TOOL_NAMES: () => TOOL_NAMES,
30
30
  WHITEBOARD_ID: () => WHITEBOARD_ID,
31
31
  WHITEBOARD_SAMPLE: () => WHITEBOARD_SAMPLE,
32
+ annotationPanel: () => annotationPanel,
32
33
  default: () => factory,
33
34
  mountWhiteboard: () => mountWhiteboard,
35
+ onMessageSent: () => onMessageSent,
36
+ setPendingBlock: () => setPendingBlock,
34
37
  themeMode: () => themeMode,
38
+ updateDockLayer: () => updateDockLayer,
35
39
  viewFor: () => viewFor,
36
40
  whiteboardFor: () => whiteboardFor
37
41
  });
@@ -1617,13 +1621,121 @@ window.__ModuleLoader__.load({
1617
1621
  return out;
1618
1622
  }
1619
1623
 
1624
+ // src/render/trunk.ts
1625
+ var TRUNK_MIN_EDGES = 3;
1626
+ function isVerticalSide(side) {
1627
+ return side === "top" || side === "bottom";
1628
+ }
1629
+ function trunkAt(box, side) {
1630
+ if (side === "top") return box.y - ROUTE_STUB;
1631
+ if (side === "bottom") return box.y + box.height + ROUTE_STUB;
1632
+ if (side === "left") return box.x - ROUTE_STUB;
1633
+ return box.x + box.width + ROUTE_STUB;
1634
+ }
1635
+ function alongSide(point, side) {
1636
+ return isVerticalSide(side) ? point.x : point.y;
1637
+ }
1638
+ function alongNormal(point, side) {
1639
+ return isVerticalSide(side) ? point.y : point.x;
1640
+ }
1641
+ function facesTrunk(point, side, at) {
1642
+ const normal = normalOf(side);
1643
+ const outward = isVerticalSide(side) ? normal.y : normal.x;
1644
+ return (at - alongNormal(point, side)) * outward > 0.5;
1645
+ }
1646
+ function keyOf(end) {
1647
+ return end.to + "\0" + end.entry + "\0" + (end.arrow ?? "");
1648
+ }
1649
+ function clearSegment(boxes, skip, x1, y1, x2, y2) {
1650
+ for (const box of boxes) {
1651
+ if (skip.includes(box)) continue;
1652
+ if (crossesBox(x1, y1, x2, y2, box)) return false;
1653
+ }
1654
+ return true;
1655
+ }
1656
+ function onOneLine(group, side, at, low, high) {
1657
+ if (isVerticalSide(group.side) !== isVerticalSide(side)) return false;
1658
+ if (group.at !== at) return false;
1659
+ return Math.min(group.to, high) - Math.max(group.from, low) > 0.5;
1660
+ }
1661
+ function planTrunks(input) {
1662
+ const groups = [];
1663
+ const groupOf = /* @__PURE__ */ new Map();
1664
+ const buckets = /* @__PURE__ */ new Map();
1665
+ for (const end of input.ends) {
1666
+ if (end.runY === void 0) continue;
1667
+ if (isVerticalSide(end.exit) !== isVerticalSide(end.entry)) continue;
1668
+ const source = input.boxOf(end.from);
1669
+ const target = input.boxOf(end.to);
1670
+ if (source === void 0 || target === void 0) continue;
1671
+ if (source === target) continue;
1672
+ const exit = input.exitPortOf(end.index);
1673
+ if (exit === void 0) continue;
1674
+ const at = trunkAt(target, end.entry);
1675
+ if (!facesTrunk(exit, end.exit, at)) continue;
1676
+ const candidate = { end, source, target, at, exit, branch: alongSide(exit, end.exit) };
1677
+ const key = keyOf(end);
1678
+ const list = buckets.get(key);
1679
+ if (list === void 0) buckets.set(key, [candidate]);
1680
+ else list.push(candidate);
1681
+ }
1682
+ for (const list of buckets.values()) {
1683
+ const bySource = /* @__PURE__ */ new Map();
1684
+ for (const candidate of list) {
1685
+ const seen = bySource.get(candidate.end.from);
1686
+ if (seen === void 0) bySource.set(candidate.end.from, [candidate]);
1687
+ else seen.push(candidate);
1688
+ }
1689
+ const members = list.filter((candidate) => bySource.get(candidate.end.from).length === 1);
1690
+ if (members.length < TRUNK_MIN_EDGES) continue;
1691
+ const target = members[0].target;
1692
+ const side = members[0].end.entry;
1693
+ const at = members[0].at;
1694
+ const entryAlong = isVerticalSide(side) ? target.x + target.width / 2 : target.y + target.height / 2;
1695
+ let lead = members[0];
1696
+ for (const candidate of members) {
1697
+ if (Math.abs(candidate.branch - entryAlong) < Math.abs(lead.branch - entryAlong) - 1e-9) lead = candidate;
1698
+ }
1699
+ let low = entryAlong;
1700
+ let high = entryAlong;
1701
+ for (const candidate of members) {
1702
+ low = Math.min(low, candidate.branch);
1703
+ high = Math.max(high, candidate.branch);
1704
+ }
1705
+ const vertical = isVerticalSide(side);
1706
+ const run = vertical ? [low, at, high, at] : [at, low, at, high];
1707
+ if (!clearSegment(input.boxes, [target], run[0], run[1], run[2], run[3])) continue;
1708
+ let blocked = false;
1709
+ for (const candidate of members) {
1710
+ const drop = vertical ? [candidate.branch, candidate.exit.y, candidate.branch, at] : [candidate.exit.x, candidate.branch, at, candidate.branch];
1711
+ if (!clearSegment(input.boxes, [candidate.source, target], drop[0], drop[1], drop[2], drop[3])) blocked = true;
1712
+ }
1713
+ if (blocked) continue;
1714
+ if (groups.some((group) => onOneLine(group, side, at, low, high))) continue;
1715
+ const id = groups.length;
1716
+ groups.push({
1717
+ id,
1718
+ target: members[0].end.to,
1719
+ side,
1720
+ at,
1721
+ entryAlong,
1722
+ from: low,
1723
+ to: high,
1724
+ lead: lead.end.index,
1725
+ members: members.map((candidate) => candidate.end.index)
1726
+ });
1727
+ for (const candidate of members) groupOf.set(candidate.end.index, id);
1728
+ }
1729
+ return { groups, groupOf };
1730
+ }
1731
+
1620
1732
  // src/render/symbol.ts
1621
- function cubicPoint(curve, t) {
1622
- const u = 1 - t;
1733
+ function cubicPoint(curve, t2) {
1734
+ const u = 1 - t2;
1623
1735
  const a = u * u * u;
1624
- const b = 3 * u * u * t;
1625
- const c = 3 * u * t * t;
1626
- const d = t * t * t;
1736
+ const b = 3 * u * u * t2;
1737
+ const c = 3 * u * t2 * t2;
1738
+ const d = t2 * t2 * t2;
1627
1739
  return {
1628
1740
  x: a * curve.from.x + b * curve.c1.x + c * curve.c2.x + d * curve.to.x,
1629
1741
  y: a * curve.from.y + b * curve.c1.y + c * curve.c2.y + d * curve.to.y
@@ -1786,7 +1898,7 @@ window.__ModuleLoader__.load({
1786
1898
  var CAPTION_CHAR_WIDTH = 6.2;
1787
1899
  var PORT_SEPARATION = 14;
1788
1900
  var PORT_SIDES = ["top", "right", "bottom", "left"];
1789
- var isVerticalSide = (side) => side === "top" || side === "bottom";
1901
+ var isVerticalSide2 = (side) => side === "top" || side === "bottom";
1790
1902
  var MULTI_EDGE_SPACING = 20;
1791
1903
  function packPorts(wanted, low, high) {
1792
1904
  const placed = [];
@@ -1802,6 +1914,14 @@ window.__ModuleLoader__.load({
1802
1914
  }
1803
1915
  return placed;
1804
1916
  }
1917
+ function centreRun(placed, low, high) {
1918
+ if (placed.length === 0) return placed;
1919
+ const first = placed[0];
1920
+ const last = placed[placed.length - 1];
1921
+ const middle = (low + high) / 2;
1922
+ const shift = Math.min(high - last, Math.max(low - first, middle - (first + last) / 2));
1923
+ return shift === 0 ? placed : placed.map((value) => value + shift);
1924
+ }
1805
1925
  function roleOfNode(node) {
1806
1926
  if (node.role === "error") return "fail";
1807
1927
  if (node.role === "warning") return "findings";
@@ -1835,42 +1955,65 @@ window.__ModuleLoader__.load({
1835
1955
  var LABEL_HEIGHT = 12;
1836
1956
  var LABEL_PAD = 2;
1837
1957
  var LABEL_STEP = 3;
1838
- function placeLabel(label, from, to, boxes, placed, segments) {
1958
+ function runsOf(points) {
1959
+ const runs = [];
1960
+ for (let index = 0; index + 1 < points.length; index += 1) {
1961
+ const from = points[index];
1962
+ const to = points[index + 1];
1963
+ const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
1964
+ if (length < 1) continue;
1965
+ runs.push({ from, to, length, index });
1966
+ }
1967
+ return runs.sort((left, right) => right.length - left.length || left.index - right.index);
1968
+ }
1969
+ function placeLabel(label, points, boxes, placed, otherRoutes) {
1839
1970
  const width = textWidth(label);
1840
1971
  const height = LABEL_HEIGHT;
1841
1972
  const spotAt = (x, y) => ({ x, y, width, height });
1842
1973
  const hitsBox = (spot, box) => spot.x - spot.width / 2 < box.x + box.width + LABEL_PAD && spot.x + spot.width / 2 > box.x - LABEL_PAD && spot.y - spot.height / 2 < box.y + box.height + LABEL_PAD && spot.y + spot.height / 2 > box.y - LABEL_PAD;
1843
1974
  const hitsSpot = (spot, other) => spot.x - spot.width / 2 < other.x + other.width / 2 + LABEL_PAD && spot.x + spot.width / 2 > other.x - other.width / 2 - LABEL_PAD && spot.y - spot.height / 2 < other.y + other.height / 2 + LABEL_PAD && spot.y + spot.height / 2 > other.y - other.height / 2 - LABEL_PAD;
1844
1975
  const hitsSegment = (spot, segment) => spot.x + spot.width / 2 > Math.min(segment.x1, segment.x2) - LABEL_PAD && spot.x - spot.width / 2 < Math.max(segment.x1, segment.x2) + LABEL_PAD && spot.y + spot.height / 2 > Math.min(segment.y1, segment.y2) - LABEL_PAD && spot.y - spot.height / 2 < Math.max(segment.y1, segment.y2) + LABEL_PAD;
1845
- const clearOfBoxes = (spot) => {
1846
- for (const box of boxes) if (hitsBox(spot, box)) return false;
1847
- for (const earlier of placed) if (hitsSpot(spot, earlier)) return false;
1848
- return true;
1849
- };
1850
- const clearOfEdges = (spot) => {
1851
- for (const segment of segments) if (hitsSegment(spot, segment)) return false;
1852
- return true;
1976
+ const collisionsAt = (spot, run) => {
1977
+ let boxHits = 0;
1978
+ let labelHits = 0;
1979
+ let edgeHits = 0;
1980
+ for (const box of boxes) if (hitsBox(spot, box)) boxHits += 1;
1981
+ for (const earlier of placed) if (hitsSpot(spot, earlier)) labelHits += 1;
1982
+ for (const route of otherRoutes) {
1983
+ for (const segment of route) {
1984
+ if (!hitsSegment(spot, segment)) continue;
1985
+ edgeHits += 1;
1986
+ break;
1987
+ }
1988
+ }
1989
+ return { run, collisions: boxHits + labelHits + edgeHits, boxes: boxHits, labels: labelHits, edges: edgeHits };
1853
1990
  };
1854
- const middle = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };
1855
- const dx = to.x - from.x;
1856
- const dy = to.y - from.y;
1857
- const length = Math.abs(dx) + Math.abs(dy);
1858
- const ux = dx / length;
1859
- const uy = dy / length;
1860
- const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
1861
- const bound = Math.max(0, length / 2 - along);
1862
- const walk = [0];
1863
- for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
1864
- const at = (offset) => spotAt(middle.x + ux * offset, middle.y + uy * offset);
1865
- for (const offset of walk) {
1866
- const spot = at(offset);
1867
- if (clearOfBoxes(spot) && clearOfEdges(spot)) return spot;
1868
- }
1869
- for (const offset of walk) {
1870
- const spot = at(offset);
1871
- if (clearOfBoxes(spot)) return spot;
1872
- }
1873
- return at(0);
1991
+ const runs = runsOf(points);
1992
+ if (runs.length === 0) {
1993
+ const point = points[0] ?? { x: 0, y: 0 };
1994
+ return { ...spotAt(point.x, point.y), run: -1, collisions: 0, boxes: 0, labels: 0, edges: 0 };
1995
+ }
1996
+ let best;
1997
+ for (const [runIndex, run] of runs.entries()) {
1998
+ const dx = run.to.x - run.from.x;
1999
+ const dy = run.to.y - run.from.y;
2000
+ const ux = dx / run.length;
2001
+ const uy = dy / run.length;
2002
+ const middle = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2 };
2003
+ const along = Math.abs(ux) * width / 2 + Math.abs(uy) * height / 2;
2004
+ const bound = Math.max(0, run.length / 2 - along);
2005
+ const walk = [0];
2006
+ for (let step = LABEL_STEP; step <= bound + 1e-3; step += LABEL_STEP) walk.push(step, -step);
2007
+ for (const offset of walk) {
2008
+ const spot = spotAt(middle.x + ux * offset, middle.y + uy * offset);
2009
+ const placement = collisionsAt(spot, runIndex);
2010
+ if (placement.collisions === 0) return { ...spot, ...placement };
2011
+ if (best === void 0 || placement.collisions < best.collisions || placement.collisions === best.collisions && placement.boxes + placement.labels < best.boxes + best.labels) {
2012
+ best = { ...spot, ...placement };
2013
+ }
2014
+ }
2015
+ }
2016
+ return best;
1874
2017
  }
1875
2018
  function drawnExtent(shapes) {
1876
2019
  let width = 0;
@@ -1898,10 +2041,24 @@ window.__ModuleLoader__.load({
1898
2041
  case "title":
1899
2042
  consider(textWidth(shape.lines?.[0] ?? "") + shape.x, shape.y + 12);
1900
2043
  break;
2044
+ case "junction":
2045
+ break;
1901
2046
  }
1902
2047
  }
1903
2048
  return { width, height };
1904
2049
  }
2050
+ function boxesCrossedBy(points, from, to, boxes) {
2051
+ const crossed = [];
2052
+ for (let index = 0; index + 1 < points.length; index += 1) {
2053
+ const a = points[index];
2054
+ const b = points[index + 1];
2055
+ for (const box of boxes) {
2056
+ if (box === from || box === to) continue;
2057
+ if (crossesBox(a.x, a.y, b.x, b.y, box)) crossed.push(box);
2058
+ }
2059
+ }
2060
+ return crossed;
2061
+ }
1905
2062
  function paintWith(diagram, overrides = {}, options = {}) {
1906
2063
  const colours = palette(overrides, options);
1907
2064
  const shapes = [];
@@ -1928,6 +2085,7 @@ window.__ModuleLoader__.load({
1928
2085
  const text2 = textOf(node);
1929
2086
  shapes.push({
1930
2087
  kind: "box",
2088
+ id: node.id,
1931
2089
  x: box.x,
1932
2090
  y: box.y,
1933
2091
  width: box.width,
@@ -1965,21 +2123,100 @@ window.__ModuleLoader__.load({
1965
2123
  endsAt.set(edge.from, (endsAt.get(edge.from) ?? 0) + 1);
1966
2124
  endsAt.set(edge.to, (endsAt.get(edge.to) ?? 0) + 1);
1967
2125
  }
2126
+ const spanOf2 = (boxId, side, corner) => {
2127
+ const box = endBox(boxId);
2128
+ const vertical = side === "top" || side === "bottom";
2129
+ const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
2130
+ const within = (low, high) => ({ low, high: Math.max(low, high) });
2131
+ switch (shapeOf(boxId)) {
2132
+ // The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
2133
+ case "diamond":
2134
+ case "circle":
2135
+ return within(middle, middle);
2136
+ case "hexagon":
2137
+ return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
2138
+ case "stadium": {
2139
+ const radius = stadiumRadius(box);
2140
+ return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
2141
+ }
2142
+ case "cylinder": {
2143
+ if (vertical) return within(middle, middle);
2144
+ const cap = cylinderCaps(box).cap;
2145
+ return within(box.y + cap, box.y + box.height - cap);
2146
+ }
2147
+ default: {
2148
+ const inset = corner ? 2 : vertical ? PORT_INSET : 8;
2149
+ return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
2150
+ }
2151
+ }
2152
+ };
2153
+ const clampTo = (boxId, side, value, corner) => {
2154
+ const span = spanOf2(boxId, side, corner);
2155
+ return Math.min(span.high, Math.max(span.low, value));
2156
+ };
2157
+ const pointOn = (boxId, side, along) => {
2158
+ const box = endBox(boxId);
2159
+ if (side === "left") return { x: box.x, y: along };
2160
+ if (side === "right") return { x: box.x + box.width, y: along };
2161
+ if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
2162
+ return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
2163
+ };
1968
2164
  const claimed = /* @__PURE__ */ new Map();
1969
2165
  const claimedAt = /* @__PURE__ */ new Map();
1970
2166
  const claim = (id, index, end, side) => {
1971
2167
  claimed.set(endKey(index, end), side);
1972
- claimedAt.set(id, [...claimedAt.get(id) ?? [], side]);
2168
+ claimedAt.set(id, [...claimedAt.get(id) ?? [], { index, side }]);
2169
+ };
2170
+ const takenSides = (id, except) => (claimedAt.get(id) ?? []).filter((entry) => entry.index !== except).map((entry) => entry.side);
2171
+ const sideMiddle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2172
+ const sidePoint = (boxId, side) => pointOn(boxId, side, sideMiddle(endBox(boxId), side));
2173
+ const facesAway2 = (box, other, side) => {
2174
+ const out = normalOf(side);
2175
+ const dx = other.x + other.width / 2 - (box.x + box.width / 2);
2176
+ const dy = other.y + other.height / 2 - (box.y + box.height / 2);
2177
+ return out.x * dx + out.y * dy < 0;
2178
+ };
2179
+ const stubAlong = (pinnedId, pinnedSide, freeSide) => {
2180
+ const point = sidePoint(pinnedId, pinnedSide);
2181
+ const out = normalOf(pinnedSide);
2182
+ return isVerticalSide2(freeSide) ? point.x + out.x * ROUTE_STUB : point.y + out.y * ROUTE_STUB;
1973
2183
  };
1974
- const bestSide = (id, other) => {
2184
+ const offers = (boxId, side, value) => {
2185
+ const span = spanOf2(boxId, side, true);
2186
+ return value >= span.low - 0.01 && value <= span.high + 0.01;
2187
+ };
2188
+ const bendBudget = (fromId, toId, exit, entry, index) => {
2189
+ const from = endBox(fromId);
2190
+ const to = endBox(toId);
2191
+ if (facesAway2(from, to, exit) || facesAway2(to, from, entry)) return 4;
2192
+ if (isVerticalSide2(exit) === isVerticalSide2(entry)) {
2193
+ const out = normalOf(exit);
2194
+ const into = normalOf(entry);
2195
+ const sameWay = out.x === -into.x && out.y === -into.y;
2196
+ const fromSpan = spanOf2(fromId, exit, true);
2197
+ const toSpan = spanOf2(toId, entry, true);
2198
+ const meet = fromSpan.low <= toSpan.high + 0.01 && toSpan.low <= fromSpan.high + 0.01;
2199
+ return sameWay && meet ? 0 : 2;
2200
+ }
2201
+ const horizontalId = isVerticalSide2(exit) ? toId : fromId;
2202
+ const horizontalSide = isVerticalSide2(exit) ? entry : exit;
2203
+ const verticalId = isVerticalSide2(exit) ? fromId : toId;
2204
+ const verticalSide = isVerticalSide2(exit) ? exit : entry;
2205
+ if (!offers(verticalId, verticalSide, stubAlong(horizontalId, horizontalSide, verticalSide))) return 2;
2206
+ const crowded = takenSides(fromId, index).includes(exit) || takenSides(toId, index).includes(entry);
2207
+ return crowded ? 2 : 1;
2208
+ };
2209
+ const bestSide = (id, otherId, end, partner, index) => {
1975
2210
  const box = endBox(id);
2211
+ const other = endBox(otherId);
1976
2212
  const centre = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
1977
2213
  const toward = { x: other.x + other.width / 2 - centre.x, y: other.y + other.height / 2 - centre.y };
1978
2214
  const reach = Math.hypot(toward.x, toward.y);
1979
- const taken = claimedAt.get(id) ?? [];
2215
+ const taken = takenSides(id, index);
1980
2216
  let best;
1981
2217
  for (const side of PORT_SIDES) {
1982
2218
  if (taken.includes(side)) continue;
2219
+ const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
1983
2220
  const normal = normalOf(side);
1984
2221
  const score = reach === 0 ? 0 : (normal.x * toward.x + normal.y * toward.y) / reach;
1985
2222
  const point = anchor(box, side);
@@ -1987,11 +2224,43 @@ window.__ModuleLoader__.load({
1987
2224
  Math.max(other.x - point.x, 0, point.x - (other.x + other.width)),
1988
2225
  Math.max(other.y - point.y, 0, point.y - (other.y + other.height))
1989
2226
  );
1990
- const better = best === void 0 || score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && near < best.near - 1e-9;
1991
- if (better) best = { side, score, near };
2227
+ const partnerPoint = anchor(other, partner);
2228
+ const ground = Math.abs(point.x - partnerPoint.x) + Math.abs(point.y - partnerPoint.y);
2229
+ const better = best === void 0 || bends < best.bends || bends === best.bends && (score > best.score + 1e-9 || Math.abs(score - best.score) <= 1e-9 && (near < best.near - 1e-9 || Math.abs(near - best.near) <= 1e-9 && ground < best.ground - 1e-9));
2230
+ if (better) best = { side, bends, score, near, ground };
1992
2231
  }
1993
2232
  return best?.side;
1994
2233
  };
2234
+ const bestPair = (fromId, toId, index) => {
2235
+ const approach = approachSides(endBox(fromId), endBox(toId));
2236
+ const pairs = [
2237
+ { exit: "bottom", entry: "top" },
2238
+ { exit: "top", entry: "bottom" },
2239
+ { exit: "right", entry: "left" },
2240
+ { exit: "left", entry: "right" }
2241
+ ];
2242
+ let best = approach;
2243
+ let bestBends = bendBudget(fromId, toId, approach.exit, approach.entry, index);
2244
+ for (const pair of pairs) {
2245
+ const bends = bendBudget(fromId, toId, pair.exit, pair.entry, index);
2246
+ if (bends >= bestBends) continue;
2247
+ best = pair;
2248
+ bestBends = bends;
2249
+ }
2250
+ return best;
2251
+ };
2252
+ const bestEnd = (id, otherId, end, partner, index) => {
2253
+ const approach = end === "exit" ? approachSides(endBox(id), endBox(otherId)).exit : approachSides(endBox(otherId), endBox(id)).entry;
2254
+ let best = approach;
2255
+ let bestBends = end === "exit" ? bendBudget(id, otherId, approach, partner, index) : bendBudget(otherId, id, partner, approach, index);
2256
+ for (const side of PORT_SIDES) {
2257
+ const bends = end === "exit" ? bendBudget(id, otherId, side, partner, index) : bendBudget(otherId, id, partner, side, index);
2258
+ if (bends >= bestBends) continue;
2259
+ best = side;
2260
+ bestBends = bends;
2261
+ }
2262
+ return best;
2263
+ };
1995
2264
  diagram.edges.forEach((edge, index) => {
1996
2265
  if (edge.from === edge.to) return;
1997
2266
  const source = endBox(edge.from);
@@ -2018,7 +2287,9 @@ window.__ModuleLoader__.load({
2018
2287
  const id = end === "exit" ? edge.from : edge.to;
2019
2288
  if ((endsAt.get(id) ?? 0) < 2) continue;
2020
2289
  if (!hasDiscretePorts(shapeOf(id))) continue;
2021
- const side = bestSide(id, end === "exit" ? target : source);
2290
+ const approached = approachSides(source, target);
2291
+ const partner = claimed.get(endKey(index, end === "exit" ? "entry" : "exit")) ?? (end === "exit" ? approached.entry : approached.exit);
2292
+ const side = bestSide(id, end === "exit" ? edge.to : edge.from, end, partner, index);
2022
2293
  if (side === void 0) continue;
2023
2294
  claim(id, index, end, side);
2024
2295
  }
@@ -2027,20 +2298,22 @@ window.__ModuleLoader__.load({
2027
2298
  const source = endBox(edge.from);
2028
2299
  const target = endBox(edge.to);
2029
2300
  if (source === void 0 || target === void 0 || edge.from === edge.to) return void 0;
2030
- const sides2 = placement.across.has(index) ? { exit: "right", entry: "left" } : approachSides(source, target);
2301
+ if (placement.across.has(index)) return { exit: "right", entry: "left" };
2031
2302
  const lane = placement.lanes.get(index);
2032
- if (lane === void 0 || lane.points.length === 0) {
2303
+ const down = target.y > source.y;
2304
+ if (lane !== void 0 && lane.points.length > 0) {
2033
2305
  return {
2034
- exit: claimed.get(endKey(index, "exit")) ?? sides2.exit,
2035
- entry: claimed.get(endKey(index, "entry")) ?? sides2.entry
2306
+ exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
2307
+ entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
2308
+ at: lane.points[0].x
2036
2309
  };
2037
2310
  }
2038
- const down = target.y > source.y;
2039
- return {
2040
- exit: claimed.get(endKey(index, "exit")) ?? (down ? "bottom" : "top"),
2041
- entry: claimed.get(endKey(index, "entry")) ?? (down ? "top" : "bottom"),
2042
- at: lane.points[0].x
2043
- };
2311
+ const exit = claimed.get(endKey(index, "exit"));
2312
+ const entry = claimed.get(endKey(index, "entry"));
2313
+ if (exit !== void 0 && entry !== void 0) return { exit, entry };
2314
+ if (exit !== void 0) return { exit, entry: bestEnd(edge.to, edge.from, "entry", exit, index) };
2315
+ if (entry !== void 0) return { exit: bestEnd(edge.from, edge.to, "exit", entry, index), entry };
2316
+ return bestPair(edge.from, edge.to, index);
2044
2317
  });
2045
2318
  const laneClear = (points, x, from, to) => {
2046
2319
  for (let at = 0; at + 1 < points.length; at += 1) {
@@ -2079,50 +2352,12 @@ window.__ModuleLoader__.load({
2079
2352
  }
2080
2353
  const wanted = /* @__PURE__ */ new Map();
2081
2354
  const slotKey = (boxId, side, index) => boxId + ":" + side + ":" + String(index);
2082
- const spanOf2 = (boxId, side, corner) => {
2083
- const box = endBox(boxId);
2084
- const vertical = side === "top" || side === "bottom";
2085
- const middle = vertical ? box.x + box.width / 2 : box.y + box.height / 2;
2086
- const within = (low, high) => ({ low, high: Math.max(low, high) });
2087
- switch (shapeOf(boxId)) {
2088
- // The four vertices of a diamond, and the four cardinal points of an ellipse, are the middles of its sides.
2089
- case "diamond":
2090
- case "circle":
2091
- return within(middle, middle);
2092
- case "hexagon":
2093
- return vertical ? within(box.x + hexagonCut(box.width), box.x + box.width - hexagonCut(box.width)) : within(middle, middle);
2094
- case "stadium": {
2095
- const radius = stadiumRadius(box);
2096
- return vertical ? within(box.x + radius, box.x + box.width - radius) : within(middle, middle);
2097
- }
2098
- case "cylinder": {
2099
- if (vertical) return within(middle, middle);
2100
- const cap = cylinderCaps(box).cap;
2101
- return within(box.y + cap, box.y + box.height - cap);
2102
- }
2103
- default: {
2104
- const inset = corner ? 2 : vertical ? PORT_INSET : 8;
2105
- return vertical ? within(box.x + inset, box.x + box.width - inset) : within(box.y + inset, box.y + box.height - inset);
2106
- }
2107
- }
2108
- };
2109
- const clampTo = (boxId, side, value, corner) => {
2110
- const span = spanOf2(boxId, side, corner);
2111
- return Math.min(span.high, Math.max(span.low, value));
2112
- };
2113
- const pointOn = (boxId, side, along) => {
2114
- const box = endBox(boxId);
2115
- if (side === "left") return { x: box.x, y: along };
2116
- if (side === "right") return { x: box.x + box.width, y: along };
2117
- if (side === "top") return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "top").y : box.y };
2118
- return { x: along, y: shapeOf(boxId) === "cylinder" ? cylinderApex(box, "bottom").y : box.y + box.height };
2119
- };
2120
2355
  for (const [index, entry] of facing.entries()) {
2121
2356
  if (entry === void 0) continue;
2122
2357
  const edge = diagram.edges[index];
2123
2358
  const from = endBox(edge.from);
2124
2359
  const to = endBox(edge.to);
2125
- const middle = (box, side) => isVerticalSide(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2360
+ const middle = (box, side) => isVerticalSide2(side) ? box.x + box.width / 2 : box.y + box.height / 2;
2126
2361
  const fromMiddle = middle(from, entry.exit);
2127
2362
  const toMiddle = middle(to, entry.entry);
2128
2363
  const corner = entry.at !== void 0;
@@ -2133,14 +2368,14 @@ window.__ModuleLoader__.load({
2133
2368
  const chosen = entry.at + (laneShift.get(index) ?? 0);
2134
2369
  exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2135
2370
  entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2136
- } else if (isVerticalSide(entry.exit) === isVerticalSide(entry.entry)) {
2371
+ } else if (isVerticalSide2(entry.exit) === isVerticalSide2(entry.entry)) {
2137
2372
  const target = spanOf2(edge.to, entry.entry, false);
2138
2373
  const chosen = (fromMiddle >= target.low && fromMiddle <= target.high ? fromMiddle : clampTo(edge.from, entry.exit, toMiddle, false)) + offset;
2139
2374
  exitWanted = clampTo(edge.from, entry.exit, chosen, corner);
2140
2375
  entryWanted = clampTo(edge.to, entry.entry, chosen, corner);
2141
2376
  } else {
2142
- exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, anchor(to, entry.entry).x + normalOf(entry.entry).x * ROUTE_STUB, false) + offset;
2143
- entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, anchor(from, entry.exit).x + normalOf(entry.exit).x * ROUTE_STUB, false) + offset;
2377
+ exitWanted = hasDiscretePorts(shapeOf(edge.from)) ? fromMiddle : clampTo(edge.from, entry.exit, stubAlong(edge.to, entry.entry, entry.exit), false) + offset;
2378
+ entryWanted = hasDiscretePorts(shapeOf(edge.to)) ? toMiddle : clampTo(edge.to, entry.entry, stubAlong(edge.from, entry.exit, entry.entry), false) + offset;
2144
2379
  }
2145
2380
  wanted.set(slotKey(edge.from, entry.exit, index), exitWanted);
2146
2381
  wanted.set(slotKey(edge.to, entry.entry, index), entryWanted);
@@ -2189,6 +2424,109 @@ window.__ModuleLoader__.load({
2189
2424
  }
2190
2425
  if (!moved) break;
2191
2426
  }
2427
+ const trunkEnds = [];
2428
+ diagram.edges.forEach((edge, index) => {
2429
+ const entry = facing[index];
2430
+ if (entry === void 0) return;
2431
+ const runY = placement.runs.get(index);
2432
+ trunkEnds.push({
2433
+ index,
2434
+ from: edge.from,
2435
+ to: edge.to,
2436
+ exit: entry.exit,
2437
+ entry: entry.entry,
2438
+ ...edge.arrow === void 0 ? {} : { arrow: edge.arrow },
2439
+ ...runY === void 0 ? {} : { runY }
2440
+ });
2441
+ });
2442
+ const planFor = (exitPortOf) => planTrunks({
2443
+ ends: trunkEnds,
2444
+ // Only a node's own box is a trunk target: an arrow into a boundary points past it, not at it.
2445
+ boxOf: (id) => boxes.get(id),
2446
+ boxes: boxList,
2447
+ exitPortOf
2448
+ });
2449
+ const packedExit = (index) => {
2450
+ const entry = facing[index];
2451
+ if (entry === void 0) return void 0;
2452
+ const edge = diagram.edges[index];
2453
+ const coordinate = placedPort.get(slotKey(edge.from, entry.exit, index));
2454
+ return coordinate === void 0 ? void 0 : pointOn(edge.from, entry.exit, coordinate);
2455
+ };
2456
+ const planned = planFor(packedExit);
2457
+ const onLane = (index, coordinate) => {
2458
+ if (planned.groupOf.has(index)) return false;
2459
+ const lane = placement.lanes.get(index);
2460
+ if (lane === void 0 || lane.points.length === 0) return false;
2461
+ return Math.abs(lane.points[0].x - coordinate) <= 0.5;
2462
+ };
2463
+ const rows = [];
2464
+ const rowOf = /* @__PURE__ */ new Map();
2465
+ for (const [key, list] of sides) {
2466
+ const [boxId, side] = key.split(":");
2467
+ const span = spanOf2(boxId, side, true);
2468
+ const slots = [...list].map((entry) => ({ index: entry.index, at: placedPort.get(slotKey(boxId, side, entry.index)) })).sort((left, right) => left.at - right.at || left.index - right.index);
2469
+ const centred = centreRun(
2470
+ slots.map((slot) => slot.at),
2471
+ span.low,
2472
+ span.high
2473
+ );
2474
+ const first = slots[0];
2475
+ const row = {
2476
+ boxId,
2477
+ side,
2478
+ indexes: slots.map((slot) => slot.index),
2479
+ at: slots.map((slot) => slot.at),
2480
+ position: new Map(slots.map((slot, at) => [slot.index, at])),
2481
+ shift: centred[0] - first.at,
2482
+ allowed: true
2483
+ };
2484
+ rows.push(row);
2485
+ rowOf.set(key, row);
2486
+ }
2487
+ const movedAt = (row, at) => row.at[at] + (row.allowed ? row.shift : 0);
2488
+ const partnerOf = (boxId, side, index) => {
2489
+ const entry = facing[index];
2490
+ if (entry === void 0) return void 0;
2491
+ const edge = diagram.edges[index];
2492
+ const isExit = edge.from === boxId && entry.exit === side;
2493
+ const groupId = planned.groupOf.get(index);
2494
+ if (groupId !== void 0) {
2495
+ if (!isExit) return void 0;
2496
+ const group = planned.groups[groupId];
2497
+ if (isVerticalSide2(side) !== isVerticalSide2(group.side)) return void 0;
2498
+ return { coordinate: group.entryAlong };
2499
+ }
2500
+ const partnerId = isExit ? edge.to : edge.from;
2501
+ const partnerSide = isExit ? entry.entry : entry.exit;
2502
+ if (isVerticalSide2(side) !== isVerticalSide2(partnerSide)) return void 0;
2503
+ const coordinate = placedPort.get(slotKey(partnerId, partnerSide, index));
2504
+ if (coordinate === void 0) return void 0;
2505
+ const row = rowOf.get(partnerId + ":" + partnerSide);
2506
+ return { coordinate, ...row === void 0 ? {} : { row, at: row.position.get(index) } };
2507
+ };
2508
+ for (let pass = 0; pass < rows.length; pass += 1) {
2509
+ let refused = false;
2510
+ for (const row of rows) {
2511
+ if (!row.allowed || row.shift === 0) continue;
2512
+ const pinned = row.indexes.some((index, at) => {
2513
+ if (onLane(index, row.at[at])) return true;
2514
+ const partner = partnerOf(row.boxId, row.side, index);
2515
+ if (partner === void 0) return false;
2516
+ if (Math.abs(row.at[at] - partner.coordinate) > 0.5) return false;
2517
+ const partnerAt = partner.row === void 0 ? partner.coordinate : movedAt(partner.row, partner.at);
2518
+ return Math.abs(movedAt(row, at) - partnerAt) > 0.5;
2519
+ });
2520
+ if (!pinned) continue;
2521
+ row.allowed = false;
2522
+ refused = true;
2523
+ }
2524
+ if (!refused) break;
2525
+ }
2526
+ for (const row of rows) {
2527
+ if (!row.allowed || row.shift === 0) continue;
2528
+ row.indexes.forEach((index, at) => placedPort.set(slotKey(row.boxId, row.side, index), movedAt(row, at)));
2529
+ }
2192
2530
  const portOf = (boxId, side, edgeIndex) => {
2193
2531
  const coordinate = placedPort.get(slotKey(boxId, side, edgeIndex));
2194
2532
  return pointOn(boxId, side, coordinate);
@@ -2204,16 +2542,9 @@ window.__ModuleLoader__.load({
2204
2542
  entrySide: entry.entry
2205
2543
  });
2206
2544
  }
2207
- const longestRun = (points) => {
2208
- let best = { from: points[0], to: points[points.length - 1], length: -1 };
2209
- for (let index = 0; index + 1 < points.length; index += 1) {
2210
- const from = points[index];
2211
- const to = points[index + 1];
2212
- const length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
2213
- if (length > best.length) best = { from, to, length };
2214
- }
2215
- return best;
2216
- };
2545
+ const trunks = planFor((index) => anchorsFor.get(index)?.exit);
2546
+ const sharedEntry = /* @__PURE__ */ new Map();
2547
+ for (const group of trunks.groups) sharedEntry.set(group.id, pointOn(group.target, group.side, group.entryAlong));
2217
2548
  const pointsOfRoute = (shape) => shape.kind === "path" ? shape.points : [{ x: shape.x1, y: shape.y1 }, { x: shape.x2, y: shape.y2 }];
2218
2549
  const segmentsOfRoute = (shape) => {
2219
2550
  const points = pointsOfRoute(shape);
@@ -2225,7 +2556,7 @@ window.__ModuleLoader__.load({
2225
2556
  }
2226
2557
  return out;
2227
2558
  };
2228
- const paintPath = (edge, points, colour) => {
2559
+ const paintPath = (edgeIndex, edge, points, colour) => {
2229
2560
  const shape = {
2230
2561
  kind: "path",
2231
2562
  points,
@@ -2237,11 +2568,11 @@ window.__ModuleLoader__.load({
2237
2568
  ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2238
2569
  };
2239
2570
  shapes.push(shape);
2240
- routes.push(shape);
2571
+ routes.push({ shape, edge: edgeIndex, fixed: false });
2241
2572
  };
2242
- const paintRoute = (edge, points, colour) => {
2573
+ const paintRoute = (edgeIndex, edge, points, colour) => {
2243
2574
  if (points.length > 2) {
2244
- paintPath(edge, points, colour);
2575
+ paintPath(edgeIndex, edge, points, colour);
2245
2576
  return;
2246
2577
  }
2247
2578
  const exit = points[0];
@@ -2260,38 +2591,31 @@ window.__ModuleLoader__.load({
2260
2591
  ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2261
2592
  };
2262
2593
  shapes.push(shape);
2263
- routes.push(shape);
2594
+ routes.push({ shape, edge: edgeIndex, fixed: false });
2264
2595
  };
2596
+ const clearOfBoxes = (points, from, to) => boxesCrossedBy(points, from, to, boxList).length === 0;
2265
2597
  const runRoute = (ports, runY) => {
2266
- const out = isVerticalSide(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
2267
- const into = isVerticalSide(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
2598
+ const out = isVerticalSide2(ports.exitSide) ? 0 : normalOf(ports.exitSide).x * ROUTE_STUB;
2599
+ const into = isVerticalSide2(ports.entrySide) ? 0 : normalOf(ports.entrySide).x * ROUTE_STUB;
2600
+ const exitRun = out !== 0 && into === 0 && Math.sign(ports.entry.x - ports.exit.x) === Math.sign(out) ? ports.entry.x : ports.exit.x + out;
2601
+ const entryRun = into !== 0 && out === 0 && Math.sign(ports.exit.x - ports.entry.x) === Math.sign(into) ? ports.exit.x : ports.entry.x + into;
2268
2602
  const points = [ports.exit];
2269
- if (out !== 0) points.push({ x: ports.exit.x + out, y: ports.exit.y });
2270
- points.push({ x: ports.exit.x + out, y: runY }, { x: ports.entry.x + into, y: runY });
2271
- if (into !== 0) points.push({ x: ports.entry.x + into, y: ports.entry.y });
2603
+ if (out !== 0) points.push({ x: exitRun, y: ports.exit.y });
2604
+ points.push({ x: exitRun, y: runY }, { x: entryRun, y: runY });
2605
+ if (into !== 0) points.push({ x: entryRun, y: ports.entry.y });
2272
2606
  points.push(ports.entry);
2273
2607
  return simplify(points);
2274
2608
  };
2275
2609
  const runFits = (ports, runY) => {
2276
- if (isVerticalSide(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
2610
+ if (isVerticalSide2(ports.exitSide) && Math.abs(runY - ports.exit.y) > 0.5) {
2277
2611
  if (Math.sign(runY - ports.exit.y) !== Math.sign(normalOf(ports.exitSide).y)) return false;
2278
2612
  }
2279
- if (isVerticalSide(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
2613
+ if (isVerticalSide2(ports.entrySide) && Math.abs(ports.entry.y - runY) > 0.5) {
2280
2614
  if (Math.sign(ports.entry.y - runY) !== -Math.sign(normalOf(ports.entrySide).y)) return false;
2281
2615
  }
2282
2616
  return true;
2283
2617
  };
2284
- const clearsBoxes = (points, from, to) => {
2285
- for (let at = 0; at + 1 < points.length; at += 1) {
2286
- const a = points[at];
2287
- const b = points[at + 1];
2288
- for (const box of boxList) {
2289
- if (box === from || box === to) continue;
2290
- if (crossesBox(a.x, a.y, b.x, b.y, box)) return false;
2291
- }
2292
- }
2293
- return true;
2294
- };
2618
+ const junctions = [];
2295
2619
  for (const [edgeIndex, edge] of diagram.edges.entries()) {
2296
2620
  const source = endBox(edge.from);
2297
2621
  const target = endBox(edge.to);
@@ -2300,7 +2624,7 @@ window.__ModuleLoader__.load({
2300
2624
  if (edge.from === edge.to) {
2301
2625
  const x = source.x + source.width;
2302
2626
  const y = source.y + source.height / 2;
2303
- shapes.push({
2627
+ const shape = {
2304
2628
  kind: "path",
2305
2629
  points: [
2306
2630
  { x, y: y - 9 },
@@ -2316,10 +2640,25 @@ window.__ModuleLoader__.load({
2316
2640
  colour,
2317
2641
  ...edge.dashed === void 0 ? {} : { dashed: edge.dashed },
2318
2642
  ...edge.arrow === void 0 ? {} : { arrow: edge.arrow }
2319
- });
2643
+ };
2644
+ shapes.push(shape);
2645
+ routes.push({ shape, edge: edgeIndex, fixed: true });
2320
2646
  continue;
2321
2647
  }
2322
2648
  const anchors = anchorsFor.get(edgeIndex);
2649
+ const groupId = trunks.groupOf.get(edgeIndex);
2650
+ if (groupId !== void 0 && anchors !== void 0) {
2651
+ const group = trunks.groups[groupId];
2652
+ const entry = sharedEntry.get(groupId);
2653
+ const branch = isVerticalSide2(group.side) ? [anchors.exit, { x: anchors.exit.x, y: group.at }, { x: entry.x, y: group.at }, entry] : [anchors.exit, { x: group.at, y: anchors.exit.y }, { x: group.at, y: entry.y }, entry];
2654
+ const points = simplify(branch);
2655
+ if (clearOfBoxes(points, source, target)) {
2656
+ paintRoute(edgeIndex, edge, points, colour);
2657
+ const meet = isVerticalSide2(group.side) ? { x: anchors.exit.x, y: group.at } : { x: group.at, y: anchors.exit.y };
2658
+ junctions.push({ kind: "junction", x: meet.x, y: meet.y, colour, trunk: group.id });
2659
+ continue;
2660
+ }
2661
+ }
2323
2662
  const lane = placement.lanes.get(edgeIndex);
2324
2663
  if (lane !== void 0 && lane.points.length > 0 && anchors !== void 0) {
2325
2664
  const shift = laneShift.get(edgeIndex) ?? 0;
@@ -2334,32 +2673,76 @@ window.__ModuleLoader__.load({
2334
2673
  }
2335
2674
  if (Math.abs(previous.x - anchors.entry.x) > 0.5 && Math.abs(previous.y - anchors.entry.y) > 0.5) points.push({ x: anchors.entry.x, y: previous.y });
2336
2675
  points.push(anchors.entry);
2337
- paintPath(edge, simplify(points), colour);
2338
- continue;
2676
+ const simplified = simplify(points);
2677
+ if (clearOfBoxes(simplified, source, target)) {
2678
+ paintPath(edgeIndex, edge, simplified, colour);
2679
+ continue;
2680
+ }
2339
2681
  }
2340
2682
  const runY = placement.runs.get(edgeIndex);
2341
2683
  if (runY !== void 0 && anchors !== void 0 && runFits(anchors, runY)) {
2342
2684
  const points = runRoute(anchors, runY);
2343
- const mixed = isVerticalSide(anchors.exitSide) !== isVerticalSide(anchors.entrySide);
2344
- if (!mixed || clearsBoxes(points, source, target)) {
2345
- paintRoute(edge, points, colour);
2685
+ if (clearOfBoxes(points, source, target)) {
2686
+ paintRoute(edgeIndex, edge, points, colour);
2346
2687
  continue;
2347
2688
  }
2348
2689
  }
2349
- paintRoute(edge, routeEdge(source, target, boxList, anchors).points, colour);
2690
+ paintRoute(edgeIndex, edge, routeEdge(source, target, boxList, anchors).points, colour);
2691
+ }
2692
+ for (const dot of junctions) shapes.push(dot);
2693
+ const boxEntries = Array.from(boxes.entries());
2694
+ const routingFailures = [];
2695
+ for (const { shape, edge } of routes) {
2696
+ const diagramEdge = diagram.edges[edge];
2697
+ const source = endBox(diagramEdge.from);
2698
+ const target = endBox(diagramEdge.to);
2699
+ if (source === void 0 || target === void 0) continue;
2700
+ for (const segment of segmentsOfRoute(shape)) {
2701
+ for (const [id, box] of boxEntries) {
2702
+ if (box === source || box === target) continue;
2703
+ if (!crossesBox(segment.x1, segment.y1, segment.x2, segment.y2, box)) continue;
2704
+ routingFailures.push({
2705
+ edge,
2706
+ from: diagramEdge.from,
2707
+ to: diagramEdge.to,
2708
+ box: id,
2709
+ x1: segment.x1,
2710
+ y1: segment.y1,
2711
+ x2: segment.x2,
2712
+ y2: segment.y2
2713
+ });
2714
+ }
2715
+ }
2350
2716
  }
2351
- const routeSegments = routes.map((shape) => segmentsOfRoute(shape));
2717
+ const routeSegments = routes.map(({ shape }) => segmentsOfRoute(shape));
2352
2718
  const placedLabels = [];
2353
- routes.forEach((shape, index) => {
2354
- const run = longestRun(pointsOfRoute(shape));
2355
- let spot = { x: (run.from.x + run.to.x) / 2, y: (run.from.y + run.to.y) / 2, width: textWidth(shape.label), height: LABEL_HEIGHT };
2356
- if (shape.label !== "") {
2357
- spot = placeLabel(shape.label, run.from, run.to, boxList, placedLabels, routeSegments.flatMap((list, at) => at === index ? [] : list));
2358
- placedLabels.push(spot);
2359
- }
2360
- shape.labelX = spot.x;
2361
- shape.labelY = spot.y;
2719
+ routes.forEach(({ shape, fixed }, at) => {
2720
+ if (fixed) {
2721
+ if (shape.label !== "") {
2722
+ placedLabels.push({ x: shape.labelX, y: shape.labelY, width: textWidth(shape.label), height: LABEL_HEIGHT });
2723
+ }
2724
+ return;
2725
+ }
2726
+ if (shape.label === "") return;
2727
+ const placement2 = placeLabel(
2728
+ shape.label,
2729
+ pointsOfRoute(shape),
2730
+ boxList,
2731
+ placedLabels,
2732
+ routeSegments.filter((_, other) => other !== at)
2733
+ );
2734
+ shape.labelX = placement2.x;
2735
+ shape.labelY = placement2.y;
2736
+ shape.labelPlacement = {
2737
+ run: placement2.run,
2738
+ collisions: placement2.collisions,
2739
+ boxes: placement2.boxes,
2740
+ labels: placement2.labels,
2741
+ edges: placement2.edges
2742
+ };
2743
+ placedLabels.push({ x: placement2.x, y: placement2.y, width: placement2.width, height: placement2.height });
2362
2744
  });
2745
+ const labelCollisions = routes.reduce((total, { shape }) => total + ((shape.labelPlacement?.collisions ?? 0) > 0 ? 1 : 0), 0);
2363
2746
  shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
2364
2747
  const contentRight = Math.max(
2365
2748
  MARGIN,
@@ -2380,7 +2763,10 @@ window.__ModuleLoader__.load({
2380
2763
  const extent = drawnExtent(shapes);
2381
2764
  const canvasWidth = Math.max(width, extent.width + MARGIN);
2382
2765
  const canvasHeight = Math.max(height, extent.height + MARGIN);
2383
- return { canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes }, colours };
2766
+ return {
2767
+ canvas: { width: Math.round(canvasWidth), height: Math.round(canvasHeight), shapes, routingFailures, labelCollisions },
2768
+ colours
2769
+ };
2384
2770
  }
2385
2771
 
2386
2772
  // src/render/from-mermaid.ts
@@ -3107,13 +3493,16 @@ window.__ModuleLoader__.load({
3107
3493
  const after = shape.points[1] ?? first;
3108
3494
  return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round"' + dash(shape) + " />" + edgeMarks(first, after, before, last, shape.arrow, stroke, colours.background) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
3109
3495
  },
3496
+ // A junction dot: the visible answer to "is this a join or a crossing?". Filled in the line's own colour, so it
3497
+ // reads as the line thickening where a branch meets it rather than as a new mark on the page.
3498
+ junction: (shape, colours) => '<circle cx="' + n(shape.x) + '" cy="' + n(shape.y) + '" r="2.5" fill="' + colours[shape.colour] + '" />',
3110
3499
  title: (shape, colours) => {
3111
3500
  return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
3112
3501
  },
3113
3502
  caption: (shape, colours) => {
3114
3503
  const colour = colours[shape.colour];
3115
3504
  const lines = shape.lines.map((line, index) => '<tspan x="' + n(shape.x) + '" dy="' + (index === 0 ? 0 : 15) + '">' + escape(line) + "</tspan>").join("");
3116
- return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" text-anchor="middle" fill="' + colour + '">' + lines + "</text>";
3505
+ return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" fill="' + colour + '">' + lines + "</text>";
3117
3506
  }
3118
3507
  };
3119
3508
  function dash(shape) {
@@ -3184,7 +3573,9 @@ window.__ModuleLoader__.load({
3184
3573
  dropped += 1;
3185
3574
  continue;
3186
3575
  }
3187
- body.push(writer(shape, colours));
3576
+ const drawn = writer(shape, colours);
3577
+ const nodeId = shape.id;
3578
+ body.push(shape.kind === "box" && typeof nodeId === "string" ? '<g data-node-id="' + escape(nodeId) + '">' + drawn + "</g>" : drawn);
3188
3579
  }
3189
3580
  const header = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + n(canvas.width) + " " + n(canvas.height) + '" width="' + n(canvas.width) + '" height="' + n(canvas.height) + '" role="img"' + (label === void 0 ? "" : ' aria-label="' + escape(label) + '"') + ' font-family="system-ui, -apple-system, Segoe UI, sans-serif">';
3190
3581
  const background = '<rect x="0" y="0" width="' + n(canvas.width) + '" height="' + n(canvas.height) + '" fill="' + colours.background + '" />';
@@ -3297,8 +3688,409 @@ window.__ModuleLoader__.load({
3297
3688
  };
3298
3689
  }
3299
3690
 
3691
+ // src/annotation.ts
3692
+ function annotationBlock(annotations) {
3693
+ const count = annotations.length;
3694
+ const lines = [
3695
+ "Diagram annotations - " + String(count) + (count === 1 ? " item" : " items") + " (numbers match the picture).",
3696
+ 'Please answer each item separately, and start each answer with "Annotation N:".'
3697
+ ];
3698
+ const caption = (annotations[0]?.caption ?? "").trim();
3699
+ if (caption !== "") lines.push("Drawing: " + caption);
3700
+ annotations.forEach((annotation, index) => {
3701
+ const label = (annotation.label ?? "").trim();
3702
+ const node = label === "" || label === annotation.nodeId ? annotation.nodeId : label + " (" + annotation.nodeId + ")";
3703
+ const note = (annotation.note ?? "").trim();
3704
+ lines.push("");
3705
+ lines.push("Diagram note [" + String(index + 1) + "] node " + node + " - " + (note === "" ? "(marked, no note)" : note));
3706
+ });
3707
+ lines.push("", "Question:");
3708
+ return lines.join(String.fromCharCode(10));
3709
+ }
3710
+ var COMPOSER_MARKS = [
3711
+ "[data-focus-flow] textarea",
3712
+ "textarea[data-focus-flow]",
3713
+ '[data-focus-flow] [contenteditable="true"]',
3714
+ '[data-focus-flow][contenteditable="true"]',
3715
+ // No marker on this host - measured, not assumed. Everything this plugin writes carries
3716
+ // `data-dme-own`, so what remains is somebody else's writing surface.
3717
+ "textarea:not([data-dme-own])",
3718
+ '[contenteditable="true"]:not([data-dme-own])'
3719
+ ];
3720
+ function findComposer(doc) {
3721
+ if (doc === void 0) return void 0;
3722
+ try {
3723
+ for (const mark of COMPOSER_MARKS) {
3724
+ const found = doc.querySelector(mark);
3725
+ if (found !== null && typeof found === "object") return found;
3726
+ }
3727
+ const marked = doc.querySelector("[data-focus-flow]");
3728
+ if (marked !== null && typeof marked === "object") {
3729
+ const inner = marked.querySelector?.('textarea, [contenteditable="true"]');
3730
+ if (inner !== null && inner !== void 0 && typeof inner === "object") return inner;
3731
+ }
3732
+ return void 0;
3733
+ } catch {
3734
+ return void 0;
3735
+ }
3736
+ }
3737
+ function eventOf(type, init) {
3738
+ const scope = globalThis;
3739
+ if (scope.Event === void 0) return void 0;
3740
+ try {
3741
+ return new scope.Event(type, init);
3742
+ } catch {
3743
+ return void 0;
3744
+ }
3745
+ }
3746
+ function setValue(element, next) {
3747
+ const scope = globalThis;
3748
+ for (const ctor of [scope.HTMLTextAreaElement, scope.HTMLInputElement]) {
3749
+ const descriptor = ctor === void 0 ? void 0 : Object.getOwnPropertyDescriptor(ctor.prototype, "value");
3750
+ if (descriptor !== void 0 && typeof descriptor.set === "function") {
3751
+ descriptor.set.call(element, next);
3752
+ return;
3753
+ }
3754
+ }
3755
+ element.value = next;
3756
+ }
3757
+ function fillComposer(doc, text2) {
3758
+ const composer = findComposer(doc);
3759
+ if (composer === void 0) return "no-composer";
3760
+ const isField = typeof composer.value === "string" || composer.tagName === "TEXTAREA" || composer.tagName === "INPUT";
3761
+ if (isField) {
3762
+ const current = typeof composer.value === "string" ? composer.value : "";
3763
+ setValue(composer, current.trim() === "" ? text2 : current.replace(/\s*$/, "") + "\n\n" + text2);
3764
+ } else {
3765
+ if (composer.focus !== void 0) composer.focus();
3766
+ const editing = globalThis;
3767
+ const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3768
+ const insertHtml = () => {
3769
+ const command = editing.document?.execCommand;
3770
+ if (command === void 0 || editing.document === void 0) return false;
3771
+ const html = text2.split(String.fromCharCode(10)).map(escapeHtml).join("<br>");
3772
+ return command.call(editing.document, "insertHTML", false, html) === true;
3773
+ };
3774
+ const insertLines = () => {
3775
+ const command = editing.document?.execCommand;
3776
+ if (command === void 0 || editing.document === void 0) return false;
3777
+ const lines = text2.split(String.fromCharCode(10));
3778
+ for (let index = 0; index < lines.length; index += 1) {
3779
+ if (index > 0 && command.call(editing.document, "insertParagraph", false, "") !== true) return false;
3780
+ if (lines[index] !== "" && command.call(editing.document, "insertText", false, lines[index]) !== true) return false;
3781
+ }
3782
+ return true;
3783
+ };
3784
+ const inserted = insertHtml() || insertLines();
3785
+ if (!inserted) {
3786
+ const current = typeof composer.textContent === "string" ? composer.textContent : "";
3787
+ composer.textContent = current.trim() === "" ? text2 : current.replace(/\s*$/, "") + "\n\n" + text2;
3788
+ }
3789
+ }
3790
+ const input = eventOf("input", { bubbles: true });
3791
+ if (input !== void 0 && composer.dispatchEvent !== void 0) composer.dispatchEvent(input);
3792
+ if (composer.focus !== void 0) composer.focus();
3793
+ return "filled";
3794
+ }
3795
+ function submitComposer(doc) {
3796
+ const composer = findComposer(doc);
3797
+ if (composer === void 0 || composer.dispatchEvent === void 0) return false;
3798
+ const scope = globalThis;
3799
+ if (scope.KeyboardEvent === void 0) return false;
3800
+ try {
3801
+ composer.dispatchEvent(new scope.KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true, cancelable: true }));
3802
+ return true;
3803
+ } catch {
3804
+ return false;
3805
+ }
3806
+ }
3807
+ function composerRect(doc) {
3808
+ const composer = findComposer(doc);
3809
+ const inner = composer?.querySelector?.('textarea, [contenteditable="true"]');
3810
+ const surface = inner !== null && inner !== void 0 && typeof inner === "object" ? inner : composer;
3811
+ const measure2 = surface?.getBoundingClientRect;
3812
+ if (surface === void 0 || typeof measure2 !== "function") return void 0;
3813
+ try {
3814
+ const rect = measure2.call(surface);
3815
+ return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
3816
+ } catch {
3817
+ return void 0;
3818
+ }
3819
+ }
3820
+ function prependBlock(document, annotations, caption) {
3821
+ if (annotations.length === 0) return "";
3822
+ const block = annotationBlock(annotations.map((annotation) => ({ ...annotation, caption })));
3823
+ const composer = findComposer(document);
3824
+ if (composer === void 0) return "\u6CA1\u627E\u5230\u8F93\u5165\u6846\uFF1A\u5148\u628A\u56FE\u8D34\u8FDB\u4F1A\u8BDD\uFF0C\u6216\u624B\u52A8\u590D\u5236\u8FD9\u6BB5\u6587\u5B57";
3825
+ const isField = typeof composer.value === "string" || composer.tagName === "TEXTAREA" || composer.tagName === "INPUT";
3826
+ if (isField) {
3827
+ const current = typeof composer.value === "string" ? composer.value : "";
3828
+ setValue(composer, current.trim() === "" ? block : block + String.fromCharCode(10, 10) + current);
3829
+ } else if (fillComposer(document, block) === "no-composer") {
3830
+ return "\u6CA1\u627E\u5230\u8F93\u5165\u6846\uFF1A\u5148\u628A\u56FE\u8D34\u8FDB\u4F1A\u8BDD\uFF0C\u6216\u624B\u52A8\u590D\u5236\u8FD9\u6BB5\u6587\u5B57";
3831
+ }
3832
+ const input = eventOf("input", { bubbles: true });
3833
+ if (input !== void 0 && composer.dispatchEvent !== void 0) composer.dispatchEvent(input);
3834
+ return "\u5DF2\u653E\u5165\u8F93\u5165\u6846\uFF08" + String(annotations.length) + " \u6761\u56FE\u6CE8\uFF09";
3835
+ }
3836
+ function withoutEmptyQuestion(block) {
3837
+ return block.replace(/\n+Question:\s*$/, "");
3838
+ }
3839
+
3840
+ // src/transcript.ts
3841
+ var TRANSCRIPT_MARKER = "Diagram annotations -";
3842
+ var QUESTION = "Question:";
3843
+ var HANDLED = "data-dme-annotated";
3844
+ var ORIGINAL = "data-dme-annotation-text";
3845
+ var SELECTOR = "div, p, span, li";
3846
+ var BORDER = "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.35))";
3847
+ function annotationCountOf(text2) {
3848
+ const found = /Diagram annotations - (\d+)/.exec(text2);
3849
+ return found === null ? null : Number(found[1]);
3850
+ }
3851
+ function itemsOf(text2) {
3852
+ const items = [];
3853
+ for (const raw of text2.split(String.fromCharCode(10))) {
3854
+ const line = raw.trim();
3855
+ const head = /^Diagram note \[(\d+)\] node (.+)$/.exec(line);
3856
+ if (head !== null) {
3857
+ const rest = head[2];
3858
+ const dash2 = rest.lastIndexOf(" - ");
3859
+ items.push({ index: Number(head[1]), node: dash2 === -1 ? rest : rest.slice(0, dash2), note: dash2 === -1 ? "" : rest.slice(dash2 + 3) });
3860
+ continue;
3861
+ }
3862
+ const note = /^Note: (.*)$/.exec(line);
3863
+ if (note !== null && items.length > 0) items[items.length - 1].note = note[1];
3864
+ }
3865
+ return items;
3866
+ }
3867
+ function textNodesOf(root) {
3868
+ const out = [];
3869
+ const visit = (node) => {
3870
+ const children = node.childNodes;
3871
+ if (children === void 0 || children === null || children.length === 0) {
3872
+ if (typeof node.nodeValue === "string") out.push(node);
3873
+ return;
3874
+ }
3875
+ for (let index = 0; index < children.length; index += 1) visit(children[index]);
3876
+ };
3877
+ visit(root);
3878
+ return out;
3879
+ }
3880
+ function cutTextNodes(nodes, cut2) {
3881
+ let position = 0;
3882
+ let done = false;
3883
+ for (const node of nodes) {
3884
+ const value = typeof node.nodeValue === "string" ? node.nodeValue : "";
3885
+ const length = value.length;
3886
+ if (done) {
3887
+ position += length;
3888
+ continue;
3889
+ }
3890
+ if (cut2 <= position + length) {
3891
+ node.nodeValue = value.slice(cut2 - position).replace(/^\s+/, "");
3892
+ done = true;
3893
+ } else {
3894
+ node.nodeValue = "";
3895
+ }
3896
+ position += length;
3897
+ }
3898
+ return done;
3899
+ }
3900
+ function isForeign(node) {
3901
+ for (const name of ["data-annotation-for-dsh", "data-annotation-overlay", "data-annotation-chip", "data-annotation-tip-layer"]) {
3902
+ const value = node.getAttribute?.(name);
3903
+ if (value !== void 0 && value !== null) return true;
3904
+ }
3905
+ return false;
3906
+ }
3907
+ function holdsAnotherBlock(node) {
3908
+ const inner = node.querySelectorAll?.(SELECTOR);
3909
+ if (inner === void 0 || inner === null) return false;
3910
+ for (let index = 0; index < inner.length; index += 1) {
3911
+ const text2 = inner[index].textContent;
3912
+ if (typeof text2 === "string" && text2.indexOf(TRANSCRIPT_MARKER) !== -1) return true;
3913
+ }
3914
+ return false;
3915
+ }
3916
+ function cardFor(doc, items, title, full) {
3917
+ const card = doc.createElement?.("div");
3918
+ if (card === void 0) return void 0;
3919
+ styleOf(card, {
3920
+ position: "fixed",
3921
+ zIndex: "40",
3922
+ display: "none",
3923
+ maxWidth: "360px",
3924
+ padding: "8px 10px",
3925
+ borderRadius: "10px",
3926
+ background: "var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-layer-1, Canvas))",
3927
+ border: BORDER,
3928
+ boxShadow: "0 10px 30px rgba(0,0,0,0.28)",
3929
+ color: "var(--dsw-alias-label-primary, CanvasText)",
3930
+ font: "12px system-ui, sans-serif"
3931
+ });
3932
+ const head = doc.createElement?.("div");
3933
+ if (head !== void 0) {
3934
+ head.textContent = title;
3935
+ styleOf(head, {
3936
+ color: "var(--dsw-alias-label-secondary, GrayText)",
3937
+ paddingBottom: "6px",
3938
+ marginBottom: "4px",
3939
+ borderBottom: BORDER,
3940
+ fontSize: "11px"
3941
+ });
3942
+ card.appendChild?.(head);
3943
+ }
3944
+ for (const item of items) {
3945
+ const row = doc.createElement?.("div");
3946
+ if (row === void 0) continue;
3947
+ styleOf(row, { display: "flex", gap: "6px", alignItems: "flex-start", padding: "3px 0" });
3948
+ const badge = doc.createElement?.("span");
3949
+ if (badge !== void 0) {
3950
+ badge.textContent = String(item.index);
3951
+ styleOf(badge, {
3952
+ flex: "0 0 auto",
3953
+ width: "16px",
3954
+ height: "16px",
3955
+ lineHeight: "16px",
3956
+ textAlign: "center",
3957
+ borderRadius: "8px",
3958
+ fontSize: "10px",
3959
+ background: "var(--dsw-alias-brand-primary, #2563eb)",
3960
+ color: "#fff"
3961
+ });
3962
+ row.appendChild?.(badge);
3963
+ }
3964
+ const body = doc.createElement?.("div");
3965
+ if (body !== void 0) {
3966
+ body.textContent = item.node + (item.note === "" ? "" : String.fromCharCode(10) + item.note);
3967
+ styleOf(body, { flex: "1 1 auto", whiteSpace: "pre-wrap", wordBreak: "break-word" });
3968
+ row.appendChild?.(body);
3969
+ }
3970
+ card.appendChild?.(row);
3971
+ }
3972
+ const more = doc.createElement?.("div");
3973
+ if (more !== void 0) {
3974
+ more.textContent = full;
3975
+ styleOf(more, {
3976
+ display: "none",
3977
+ marginTop: "6px",
3978
+ paddingTop: "6px",
3979
+ borderTop: BORDER,
3980
+ fontSize: "11px",
3981
+ whiteSpace: "pre-wrap",
3982
+ color: "var(--dsw-alias-label-secondary, GrayText)",
3983
+ maxHeight: "40vh",
3984
+ overflow: "auto"
3985
+ });
3986
+ card.appendChild?.(more);
3987
+ }
3988
+ return card;
3989
+ }
3990
+ function styleOf(node, map) {
3991
+ const decl = node.style;
3992
+ if (typeof decl?.setProperty === "function") {
3993
+ const target = decl;
3994
+ for (const key of Object.keys(map)) {
3995
+ const value = map[key];
3996
+ if (value === void 0) continue;
3997
+ target.setProperty(key.replace(/[A-Z]/g, (letter) => "-" + letter.toLowerCase()), value);
3998
+ }
3999
+ return;
4000
+ }
4001
+ node.style = map;
4002
+ }
4003
+ function chipFor(doc, label, title, text2) {
4004
+ const chip = doc.createElement?.("div");
4005
+ if (chip === void 0) return void 0;
4006
+ chip.textContent = label;
4007
+ chip.setAttribute?.("role", "button");
4008
+ styleOf(chip, {
4009
+ display: "inline-flex",
4010
+ alignItems: "center",
4011
+ height: "20px",
4012
+ padding: "0 8px",
4013
+ margin: "4px 0 0",
4014
+ fontSize: "11px",
4015
+ lineHeight: "20px",
4016
+ borderRadius: "10px",
4017
+ verticalAlign: "middle",
4018
+ userSelect: "none",
4019
+ border: BORDER,
4020
+ background: "var(--dsw-alias-bg-layer-2, rgba(127,127,127,0.10))",
4021
+ color: "var(--dsw-alias-label-secondary, GrayText)",
4022
+ cursor: "pointer",
4023
+ maxWidth: "100%"
4024
+ });
4025
+ const card = cardFor(doc, itemsOf(text2), title, text2);
4026
+ if (card === void 0) return chip;
4027
+ const place2 = () => {
4028
+ const box = chip.getBoundingClientRect?.();
4029
+ styleOf(card, box === void 0 ? { display: "block" } : { display: "block", left: String(Math.max(8, box.left)) + "px", top: String(box.top + box.height + 6) + "px" });
4030
+ };
4031
+ const hide = () => {
4032
+ styleOf(card, { display: "none" });
4033
+ };
4034
+ chip.addEventListener?.("mouseenter", place2);
4035
+ chip.addEventListener?.("mouseleave", hide);
4036
+ chip.addEventListener?.("click", () => {
4037
+ if ((card.style ?? {}).display === "block") {
4038
+ hide();
4039
+ } else {
4040
+ place2();
4041
+ }
4042
+ });
4043
+ chip.appendChild?.(card);
4044
+ return chip;
4045
+ }
4046
+ function decorateTranscript(doc, label = "Notes", title = "Notes in this message") {
4047
+ if (doc === void 0 || typeof doc.querySelectorAll !== "function") return 0;
4048
+ let nodes;
4049
+ try {
4050
+ nodes = doc.querySelectorAll(SELECTOR);
4051
+ } catch {
4052
+ return 0;
4053
+ }
4054
+ let folded = 0;
4055
+ for (let index = 0; index < nodes.length; index += 1) {
4056
+ const element = nodes[index];
4057
+ const text2 = typeof element.textContent === "string" ? element.textContent : "";
4058
+ if (!text2.includes(TRANSCRIPT_MARKER)) continue;
4059
+ if (element.getAttribute?.(HANDLED) === "1") continue;
4060
+ if (isForeign(element)) continue;
4061
+ if (holdsAnotherBlock(element)) continue;
4062
+ const texts = textNodesOf(element);
4063
+ if (texts.length === 0) continue;
4064
+ const joined = texts.map((node) => typeof node.nodeValue === "string" ? node.nodeValue : "").join("");
4065
+ if (!joined.includes(TRANSCRIPT_MARKER)) continue;
4066
+ const at = joined.lastIndexOf(QUESTION);
4067
+ const cut2 = at === -1 ? joined.length : at + QUESTION.length;
4068
+ const count = annotationCountOf(joined);
4069
+ const chip = chipFor(doc, label + (count === null ? "" : " \xD7" + String(count)), title, joined);
4070
+ if (chip === void 0) continue;
4071
+ try {
4072
+ if (!cutTextNodes(texts, cut2)) continue;
4073
+ element.setAttribute?.(ORIGINAL, joined);
4074
+ element.setAttribute?.(HANDLED, "1");
4075
+ const parent = element.parentNode;
4076
+ if (parent?.insertBefore !== void 0) parent.insertBefore(chip, element.nextSibling ?? null);
4077
+ else element.appendChild?.(chip);
4078
+ folded += 1;
4079
+ } catch {
4080
+ }
4081
+ }
4082
+ return folded;
4083
+ }
4084
+ function startTranscriptDecoration(doc, label = "Notes", title = "Notes in this message") {
4085
+ decorateTranscript(doc, label, title);
4086
+ return () => {
4087
+ };
4088
+ }
4089
+
3300
4090
  // src/client.ts
3301
- var TOOL_NAMES = ["model_verify"];
4091
+ var services;
4092
+ var DRAWING_TOOL = "diagram_render";
4093
+ var TOOL_NAMES = ["model_verify", DRAWING_TOOL];
3302
4094
  function themeMode() {
3303
4095
  try {
3304
4096
  const scope = globalThis;
@@ -3319,8 +4111,1040 @@ window.__ModuleLoader__.load({
3319
4111
  }
3320
4112
  var WHITEBOARD_ID = "model-everything-diagram";
3321
4113
  var WHITEBOARD_SAMPLE = ["graph TD", " a[Idea] --> b{Feasible?}", " b -->|yes| c[Build]", " b -->|no| d[Revise]", " d --> a"].join("\n");
4114
+ function argOf(args, key) {
4115
+ if (args === null || typeof args !== "object") return void 0;
4116
+ const view = args;
4117
+ if (typeof view.value === "function") return view.value(key);
4118
+ return args[key];
4119
+ }
4120
+ var MESSAGES = {
4121
+ note: { en: "Note", zh: "\u6279\u6CE8" },
4122
+ noteOn: { en: "Note: on", zh: "\u6279\u6CE8: \u5F00" },
4123
+ noteHint: { en: "Click a node to mark it", zh: "\u70B9\u4E00\u4E2A\u8282\u70B9\u6807\u8BB0\u5B83" },
4124
+ node: { en: "Node", zh: "\u8282\u70B9" },
4125
+ add: { en: "Add", zh: "\u52A0\u5165" },
4126
+ addAndSend: { en: "Add & send", zh: "\u52A0\u5165\u5E76\u53D1\u9001" },
4127
+ close: { en: "Close", zh: "\u5173\u95ED" },
4128
+ send: { en: "Send", zh: "\u53D1\u9001" },
4129
+ clear: { en: "Clear", zh: "\u6E05\u7A7A" },
4130
+ remove: { en: "Remove", zh: "\u5220\u9664" },
4131
+ markedOnly: { en: "(marked only)", zh: "(\u53EA\u6807\u8BB0)" },
4132
+ pending: { en: "Notes", zh: "\u56FE\u6CE8" },
4133
+ fold: { en: "hide", zh: "\u6536\u8D77" },
4134
+ carried: { en: "Notes in this message", zh: "\u672C\u6D88\u606F\u643A\u5E26\u56FE\u6CE8" },
4135
+ zoomOut: { en: "Zoom out", zh: "\u7F29\u5C0F" },
4136
+ zoomIn: { en: "Zoom in", zh: "\u653E\u5927" },
4137
+ actualSize: { en: "Actual size", zh: "\u5B9E\u9645\u5927\u5C0F" },
4138
+ full: { en: "\u2922 Full", zh: "\u2922 \u5168\u5C4F" },
4139
+ exitFull: { en: "\u2921 Exit", zh: "\u2921 \u9000\u51FA" },
4140
+ diagram: { en: "Diagram", zh: "\u56FE" },
4141
+ fitOn: { en: "fit: on", zh: "fit: on" },
4142
+ fitOff: { en: "fit: off", zh: "fit: off" },
4143
+ scheme: { en: "Colours", zh: "\u914D\u8272" },
4144
+ unfold: { en: "unfold", zh: "\u5C55\u5F00" },
4145
+ placeholder: { en: "A note about this node\u2026 (Ctrl+Enter adds and sends)", zh: "\u6279\u6CE8\u2026\uFF08Ctrl+Enter \u52A0\u5165\u5E76\u53D1\u9001\uFF09" }
4146
+ };
4147
+ function languageOf() {
4148
+ const scope = globalThis;
4149
+ const asked = (scope.document?.documentElement?.lang ?? scope.navigator?.language ?? "en").toLowerCase();
4150
+ return asked.startsWith("zh") ? "zh" : "en";
4151
+ }
4152
+ function t(key) {
4153
+ return MESSAGES[key][languageOf()];
4154
+ }
4155
+ var TOKEN = {
4156
+ surface: "var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-layer-1, Canvas))",
4157
+ border: "var(--dsw-alias-border-l1, rgba(127,127,127,0.35))",
4158
+ text: "var(--dsw-alias-label-primary, CanvasText)",
4159
+ dim: "var(--dsw-alias-label-secondary, GrayText)",
4160
+ accent: "var(--dsw-alias-brand-primary, #2563eb)"
4161
+ };
4162
+ function annotationPanel(h, state, actions, options = {}) {
4163
+ const button = (label, onClick, accent = false) => h("button", { type: "button", onClick, style: { fontSize: "12px", padding: "2px 8px", borderRadius: "6px", border: "1px solid " + TOKEN.border, background: "transparent", color: accent ? TOKEN.accent : TOKEN.text, cursor: "pointer" } }, label);
4164
+ const editor = state.picked === null ? null : h(
4165
+ "div",
4166
+ {
4167
+ style: {
4168
+ ...options.floating === false ? {} : { position: "absolute", left: (state.anchor?.left ?? 10) + "px", top: (state.anchor?.top ?? 10) + "px", zIndex: 20 },
4169
+ width: "min(260px, calc(100% - 16px))",
4170
+ boxSizing: "border-box",
4171
+ display: "flex",
4172
+ flexDirection: "column",
4173
+ gap: "6px",
4174
+ padding: "8px",
4175
+ borderRadius: "10px",
4176
+ background: TOKEN.surface,
4177
+ border: "1px solid " + TOKEN.border,
4178
+ color: TOKEN.text,
4179
+ boxShadow: "0 8px 24px rgba(0,0,0,0.28)"
4180
+ }
4181
+ },
4182
+ h("div", { style: { fontSize: "12px", color: TOKEN.dim } }, t("node") + " " + state.picked.id),
4183
+ h("textarea", {
4184
+ "data-dme-own": "1",
4185
+ value: state.note,
4186
+ rows: 3,
4187
+ placeholder: t("placeholder"),
4188
+ style: { width: "100%", boxSizing: "border-box", font: "12px system-ui, sans-serif", padding: "6px", borderRadius: "6px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text, resize: "vertical" },
4189
+ onInput: (event) => actions.onNote(String(event?.target?.value ?? "")),
4190
+ onKeyDown: (event) => {
4191
+ if (event?.key === "Enter" && (event?.ctrlKey === true || event?.metaKey === true)) {
4192
+ event.preventDefault?.();
4193
+ actions.onAdd();
4194
+ }
4195
+ }
4196
+ }),
4197
+ h("div", { style: { display: "flex", gap: "6px", justifyContent: "flex-end" } }, button("\u5173\u95ED", actions.onCancel), button("\u52A0\u5165", actions.onAdd))
4198
+ );
4199
+ const docked = options.floating !== false && options.document !== void 0 ? composerRect(options.document) : void 0;
4200
+ const chip = state.pending.length === 0 || options.dockChip === false ? null : h(
4201
+ "div",
4202
+ {
4203
+ style: {
4204
+ position: "fixed",
4205
+ zIndex: 30,
4206
+ left: (docked === void 0 ? 16 : docked.left) + "px",
4207
+ transform: "translate(" + String(state.dockFix?.dx ?? 0) + "px, " + String(state.dockFix?.dy ?? 0) + "px)",
4208
+ // Anchored to the composer's own top edge, in the gap above it: the count is in sight while the
4209
+ // reader is typing, and never covers the box they are typing into.
4210
+ top: (docked === void 0 ? 16 : Math.max(8, docked.top - 46)) + "px",
4211
+ // Hugs its two buttons. A fixed width turned the control into a wide empty bar across the composer.
4212
+ display: "flex",
4213
+ flexDirection: "column",
4214
+ gap: "6px",
4215
+ padding: "6px 10px",
4216
+ borderRadius: "12px",
4217
+ width: "max-content",
4218
+ // The composer's own surface and hairline: the control reads as part of that box, not as a dialog.
4219
+ background: "var(--dsw-alias-bg-layer-1, " + TOKEN.surface + ")",
4220
+ border: "1px solid var(--dsw-alias-border-l2, " + TOKEN.border + ")",
4221
+ color: TOKEN.text,
4222
+ boxShadow: "0 8px 24px rgba(0,0,0,0.22)",
4223
+ font: "12px system-ui, sans-serif"
4224
+ }
4225
+ },
4226
+ h(
4227
+ "div",
4228
+ { "data-dme-dock": "1", style: { display: "flex", alignItems: "center", gap: "6px" } },
4229
+ h("button", { type: "button", title: "\u5C55\u5F00\uFF0C\u9010\u4E2A\u5220\u9664", onClick: () => actions.onToggle(), style: { fontSize: "12px", padding: "1px 6px", borderRadius: "6px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text, cursor: "pointer" } }, t("pending") + " \xD7" + String(state.pending.length)),
4230
+ button(t("send"), actions.onSend, true)
4231
+ ),
4232
+ state.open ? h(
4233
+ "div",
4234
+ { style: { display: "flex", flexDirection: "column", gap: "2px" } },
4235
+ ...state.pending.map(
4236
+ (item, index) => h(
4237
+ "div",
4238
+ { style: { display: "flex", alignItems: "center", gap: "6px" } },
4239
+ h("span", { style: { color: TOKEN.dim, fontSize: "11px" } }, item.nodeId),
4240
+ h("span", { style: { flex: "1 1 auto", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, item.note === "" ? t("markedOnly") : item.note),
4241
+ button(t("remove"), () => actions.onRemove(index))
4242
+ )
4243
+ )
4244
+ ) : null
4245
+ );
4246
+ const mode = state.picked === null && state.pending.length === 0 ? h("div", { style: { fontSize: "11px", color: TOKEN.dim, padding: "2px 4px" } }, t("noteHint")) : null;
4247
+ return h(
4248
+ "div",
4249
+ { style: { display: "flex", flexDirection: "column", gap: "2px", color: TOKEN.text } },
4250
+ mode,
4251
+ chip,
4252
+ editor,
4253
+ state.status === "" ? null : h("div", { style: { fontSize: "11px", color: TOKEN.dim, padding: "0 4px" } }, state.status)
4254
+ );
4255
+ }
4256
+ function sourceOf(block) {
4257
+ const record2 = (value) => value !== null && typeof value === "object" ? value : void 0;
4258
+ const asText = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
4259
+ const fromJson = (value) => {
4260
+ const raw = asText(value);
4261
+ if (raw === void 0) return void 0;
4262
+ try {
4263
+ return asText(record2(JSON.parse(raw))?.source);
4264
+ } catch {
4265
+ return void 0;
4266
+ }
4267
+ };
4268
+ const frame = record2(block);
4269
+ if (frame === void 0) return void 0;
4270
+ const fromMeta = asText(record2(frame.meta)?.source);
4271
+ if (fromMeta !== void 0) return fromMeta;
4272
+ const args = record2(frame.args);
4273
+ if (args !== void 0 && typeof args.value === "function") {
4274
+ const lazy = asText(args.value("source"));
4275
+ if (lazy !== void 0) return lazy;
4276
+ }
4277
+ const plain = asText(args?.source) ?? fromJson(frame.args);
4278
+ if (plain !== void 0) return plain;
4279
+ const call = record2(frame.call);
4280
+ const fromCall = asText(call?.source) ?? fromJson(call?.input) ?? fromJson(call?.arguments);
4281
+ if (fromCall !== void 0) return fromCall;
4282
+ return asText(record2(frame.meta)?.source);
4283
+ }
4284
+ function drawingCard(React, props, overrides) {
4285
+ const block = props.block ?? {};
4286
+ const source = sourceOf(props.block) ?? argOf(block.args, "source");
4287
+ const asked = argOf(block.args, "scheme");
4288
+ const scheme = schemeNames().includes(asked) ? asked : "default";
4289
+ const payload = props.block?.result;
4290
+ const returned = payload !== null && typeof payload === "object" ? payload.svg : void 0;
4291
+ const markup = typeof returned === "string" && returned !== "" ? returned : null;
4292
+ const hasDrawing = markup !== null || typeof source === "string" && source.trim() !== "";
4293
+ const useState = React.useState;
4294
+ if (useState === void 0) return cardSurface(React, source, markup, scheme, overrides, null, null, null, null);
4295
+ const [annotate, setAnnotate] = useState(false);
4296
+ const [picked, setPicked] = useState(null);
4297
+ const [note, setNote] = useState("");
4298
+ const [status, setStatus] = useState("");
4299
+ const document = globalThis.document;
4300
+ const caption = typeof source === "string" ? source.split(String.fromCharCode(10)).map((line) => line.trim()).find((line) => line !== "") ?? "" : "";
4301
+ const [pending, setPending] = useState([]);
4302
+ const [open, setOpen] = useState(false);
4303
+ const [sent, setSent] = useState("");
4304
+ const [report, setReport] = useState("");
4305
+ const [, setDockTick] = useState(0);
4306
+ const [dockFix, setDockFix] = useState({ dx: 0, dy: 0 });
4307
+ React.useEffect?.(() => {
4308
+ const doc = globalThis.document;
4309
+ const composer = findComposer(doc);
4310
+ const bump = () => setDockTick((n2) => n2 + 1);
4311
+ const scope = globalThis;
4312
+ scope.addEventListener?.("resize", bump);
4313
+ let observer;
4314
+ if (composer !== void 0 && composer !== null && scope.ResizeObserver !== void 0) {
4315
+ try {
4316
+ observer = new scope.ResizeObserver(bump);
4317
+ observer.observe(composer);
4318
+ } catch {
4319
+ observer = void 0;
4320
+ }
4321
+ }
4322
+ return () => {
4323
+ scope.removeEventListener?.("resize", bump);
4324
+ observer?.disconnect();
4325
+ };
4326
+ }, []);
4327
+ React.useEffect?.(() => {
4328
+ const doc = globalThis.document;
4329
+ const chip = doc?.querySelector?.("[data-dme-dock]");
4330
+ const surface = composerRect(doc);
4331
+ const landed = chip?.getBoundingClientRect?.();
4332
+ if (surface === void 0 || landed === void 0) return;
4333
+ const dx = Math.round(surface.left - landed.left);
4334
+ const dy = Math.round(surface.top - landed.height - 8 - landed.top);
4335
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) setDockFix((was) => ({ dx: was.dx + dx, dy: was.dy + dy }));
4336
+ });
4337
+ const [anchor2, setAnchor] = useState(void 0);
4338
+ setPendingBlock(pending.length === 0 ? "" : annotationBlock(pending.map((item) => ({ ...item, caption }))));
4339
+ React.useEffect?.(() => {
4340
+ try {
4341
+ return onMessageSent(() => {
4342
+ setPending([]);
4343
+ setStatus("");
4344
+ });
4345
+ } catch {
4346
+ return void 0;
4347
+ }
4348
+ }, []);
4349
+ const [zoom, setZoom] = useState(1);
4350
+ const [full, setFull] = useState(false);
4351
+ if (!hasDrawing) {
4352
+ const keys = props.block === null || typeof props.block !== "object" ? "none" : Object.keys(props.block).join(",");
4353
+ return React.createElement("div", { style: { fontSize: "12px", opacity: 0.7 } }, "no drawing in this call \xB7 block: " + (keys === "" ? "empty" : keys));
4354
+ }
4355
+ const sendAll = (list) => {
4356
+ if (list.length === 0) return;
4357
+ const text2 = annotationBlock(list.map((item) => ({ ...item, caption })));
4358
+ void (async () => {
4359
+ if (placeBlock(text2)) {
4360
+ const label = "\u56FE\u6CE8 \xD7" + String(list.length);
4361
+ attachBelowBubble(label, "");
4362
+ setSent("\u5DF2\u53D1\u9001 " + label);
4363
+ attachSentChip(label);
4364
+ setStatus("\u5DF2\u653E\u5165\u8F93\u5165\u6846\u5E76\u63D0\u4EA4\uFF08" + String(list.length) + " \u6761\u56FE\u6CE8\uFF09");
4365
+ setPending([]);
4366
+ submitDraft();
4367
+ return;
4368
+ }
4369
+ const said = prependBlock(document, list, caption);
4370
+ setStatus(said);
4371
+ if (!said.startsWith("\u5DF2")) return;
4372
+ setPending([]);
4373
+ submitComposer(document);
4374
+ })();
4375
+ };
4376
+ const onPick = (event) => {
4377
+ if (!annotate) return;
4378
+ const id = event?.target?.closest?.("[data-node-id]")?.getAttribute?.("data-node-id");
4379
+ if (typeof id === "string" && id !== "") {
4380
+ const pointer = event;
4381
+ const frame = pointer.currentTarget?.getBoundingClientRect?.();
4382
+ const node = pointer.target?.closest?.("[data-node-id]")?.getBoundingClientRect?.();
4383
+ if (frame !== void 0 && node !== void 0) {
4384
+ const width = 250;
4385
+ const left = Math.max(4, Math.min(node.left - frame.left + node.width + 10, Math.max(4, frame.width - width)));
4386
+ const top = Math.max(4, Math.min(node.top - frame.top, Math.max(4, frame.height - 40)));
4387
+ setAnchor({ left, top });
4388
+ }
4389
+ setPicked({ id });
4390
+ setStatus("");
4391
+ return;
4392
+ }
4393
+ if (note.trim() === "") setPicked(null);
4394
+ };
4395
+ const toggle = React.createElement(
4396
+ "div",
4397
+ { style: { display: "flex", gap: "6px", alignItems: "center" } },
4398
+ React.createElement("button", { type: "button", title: "Point at a node and send a note about it", onClick: () => {
4399
+ setAnnotate(!annotate);
4400
+ setStatus("");
4401
+ if (annotate) {
4402
+ setPicked(null);
4403
+ setNote("");
4404
+ }
4405
+ }, style: { fontSize: "11px", lineHeight: "16px", padding: "1px 6px", borderRadius: "5px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text, cursor: "pointer" } }, annotate ? t("noteOn") : t("note")),
4406
+ annotate && picked === null ? React.createElement("span", { style: { fontSize: "11px", opacity: 0.7 } }, "\u70B9\u4E00\u4E2A\u8282\u70B9") : null,
4407
+ React.createElement("button", {
4408
+ type: "button",
4409
+ onClick: () => {
4410
+ try {
4411
+ setReport(domReport());
4412
+ } catch (error) {
4413
+ setReport("report failed: " + String(error?.message ?? error));
4414
+ }
4415
+ },
4416
+ style: { fontSize: "11px", lineHeight: "16px", padding: "1px 6px", borderRadius: "5px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.dim, cursor: "pointer" }
4417
+ }, "\u8BCA\u65AD"),
4418
+ sent === "" ? null : React.createElement("span", {
4419
+ style: { fontSize: "11px", lineHeight: "16px", padding: "1px 6px", borderRadius: "9px", border: "1px solid " + TOKEN.border, color: TOKEN.dim, whiteSpace: "nowrap" }
4420
+ }, sent)
4421
+ );
4422
+ updateDockLayer(pending, open, {
4423
+ onToggle: () => setOpen(!open),
4424
+ onSend: () => sendAll(pending),
4425
+ onRemove: (index) => setPending(pending.filter((_, at) => at !== index))
4426
+ });
4427
+ const panel = annotate ? annotationPanel(
4428
+ (type, options, ...children) => React.createElement(type, options, ...children),
4429
+ { picked, note, pending, status, anchor: anchor2, open, dockFix },
4430
+ {
4431
+ onNote: setNote,
4432
+ onAdd: () => {
4433
+ if (picked === null) return;
4434
+ const merged = [...pending, { nodeId: picked.id, note }];
4435
+ setPending(merged);
4436
+ setNote("");
4437
+ setPicked(null);
4438
+ setStatus("\u5DF2\u653E\u5165\u8F93\u5165\u6846\uFF0C\u76F4\u63A5\u56DE\u8F66\u5373\u53EF");
4439
+ },
4440
+ onAddAndSend: () => {
4441
+ if (picked === null) return;
4442
+ const merged = [...pending, { nodeId: picked.id, note }];
4443
+ setNote("");
4444
+ setPicked(null);
4445
+ sendAll(merged);
4446
+ },
4447
+ onSend: () => sendAll(pending),
4448
+ onClear: () => {
4449
+ setPending([]);
4450
+ setStatus("");
4451
+ },
4452
+ onCancel: () => {
4453
+ setPicked(null);
4454
+ setNote("");
4455
+ setStatus("");
4456
+ },
4457
+ onToggle: () => setOpen(!open),
4458
+ onRemove: (index) => setPending(pending.filter((_, at) => at !== index))
4459
+ },
4460
+ // The count is the body-level layer; this panel keeps the editor, which still needs its anchor.
4461
+ { document, dockChip: false }
4462
+ ) : null;
4463
+ const reportPanel = report === "" ? null : React.createElement("pre", {
4464
+ style: { margin: "6px 0 0", padding: "8px", borderRadius: "8px", border: "1px solid " + TOKEN.border, background: "rgba(127,127,127,0.08)", font: "11px ui-monospace, monospace", whiteSpace: "pre-wrap", userSelect: "text", maxHeight: "220px", overflow: "auto" }
4465
+ }, report);
4466
+ return cardSurface(React, source, markup, scheme, overrides, onPick, toggle, panel, reportPanel, { zoom: Number(zoom), full: Boolean(full), onZoom: (next) => setZoom(next), onFull: (next) => setFull(next) });
4467
+ }
4468
+ function cardSurface(React, source, returned, scheme, overrides, onClick, toggle, panel, report, view = { zoom: 1, full: false, onZoom: () => {
4469
+ }, onFull: () => {
4470
+ } }) {
4471
+ if (returned === null && (typeof source !== "string" || source.trim() === "")) {
4472
+ return React.createElement("div", { style: { fontSize: "12px", opacity: 0.7 } }, "no drawing in this call");
4473
+ }
4474
+ const drawn = returned === null ? render({ source: String(source), folded: [] }, overrides, { mode: themeMode(), scheme }) : { markup: returned, refused: [], colours: palette(overrides, { mode: themeMode(), scheme }) };
4475
+ const scaled = drawn.markup.replace("<svg ", '<svg style="max-width: 100%; height: auto; display: block; margin: 0 auto" ');
4476
+ const refused = drawn.refused.length === 0 ? null : React.createElement("pre", { style: { fontSize: "11px", color: "#b7791f", whiteSpace: "pre-wrap", margin: "6px 8px" } }, drawn.refused.join(String.fromCharCode(10)));
4477
+ const chrome = { border: "1px solid " + TOKEN.border, borderRadius: "8px", overflow: "hidden", background: TOKEN.surface, color: TOKEN.text };
4478
+ const button = (label, onPress, title) => React.createElement("button", { type: "button", title, onClick: onPress, style: { fontSize: "11px", lineHeight: "16px", padding: "1px 6px", borderRadius: "5px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text, cursor: "pointer" } }, label);
4479
+ const header = React.createElement(
4480
+ "div",
4481
+ { style: { display: "flex", gap: "6px", alignItems: "center", padding: "3px 8px", borderBottom: "1px solid " + TOKEN.border, fontSize: "12px", color: TOKEN.dim } },
4482
+ React.createElement("span", null, t("diagram")),
4483
+ React.createElement("span", { style: { opacity: 0.7 } }, typeof source === "string" ? source.split(String.fromCharCode(10)).map((line) => line.trim()).find((line) => line !== "") ?? "" : ""),
4484
+ React.createElement(
4485
+ "span",
4486
+ { style: { marginLeft: "auto", display: "flex", gap: "4px", alignItems: "center" } },
4487
+ button("\u2212", () => view.onZoom(Math.max(0.5, Math.round((view.zoom - 0.25) * 100) / 100)), t("zoomOut")),
4488
+ button(Math.round(view.zoom * 100) + "%", () => view.onZoom(1), t("actualSize")),
4489
+ button("+", () => view.onZoom(Math.min(3, Math.round((view.zoom + 0.25) * 100) / 100)), t("zoomIn")),
4490
+ button(view.full ? t("exitFull") : t("full"), () => view.onFull(!view.full), t("full")),
4491
+ toggle
4492
+ )
4493
+ );
4494
+ const drawing = React.createElement(
4495
+ "div",
4496
+ {
4497
+ // CSS zoom, not a transform: it reflows, so the scroll area grows with the picture and nothing is clipped.
4498
+ style: { zoom: view.zoom, cursor: onClick === null ? void 0 : "crosshair" },
4499
+ onClick: onClick === null ? void 0 : onClick,
4500
+ dangerouslySetInnerHTML: { __html: scaled }
4501
+ }
4502
+ );
4503
+ const body = React.createElement(
4504
+ "div",
4505
+ { style: { position: "relative", background: drawn.colours.background, padding: "6px", maxHeight: view.full ? "none" : "260px", overflow: "auto", flex: "1 1 auto", minHeight: "0" } },
4506
+ drawing,
4507
+ panel,
4508
+ report
4509
+ );
4510
+ const frame = view.full ? { position: "fixed", inset: "16px", zIndex: 60, display: "flex", flexDirection: "column", ...chrome, boxShadow: "0 12px 40px rgba(0,0,0,0.35)" } : { display: "flex", flexDirection: "column", ...chrome };
4511
+ const children = [header, body, refused].filter((child) => child !== null);
4512
+ return React.createElement("div", { style: frame }, ...children);
4513
+ }
4514
+ function currentSessionId() {
4515
+ const sessions = services?.get?.("sessions");
4516
+ const snapshot = sessions?.list?.getSnapshot?.();
4517
+ if (typeof snapshot?.current === "string" && snapshot.current !== "") return snapshot.current;
4518
+ const workspace = services?.get?.("uiWorkspace");
4519
+ const main = workspace?.mainReference?.sessionId;
4520
+ if (typeof main === "string" && main !== "") return main;
4521
+ const picked = workspace?.selection?.getSnapshot?.()?.sessionId;
4522
+ if (typeof picked === "string" && picked !== "") return picked;
4523
+ const storage = globalThis.localStorage;
4524
+ const raw = storage?.getItem?.("dsh.sessions.current");
4525
+ if (typeof raw === "string" && raw !== "") {
4526
+ try {
4527
+ const parsed = JSON.parse(raw);
4528
+ if (typeof parsed?.sessionId === "string" && parsed.sessionId !== "") return parsed.sessionId;
4529
+ } catch {
4530
+ }
4531
+ }
4532
+ return void 0;
4533
+ }
4534
+ function draftShell() {
4535
+ const conversation = services?.get?.("conversation");
4536
+ const sessions = services?.get?.("sessions");
4537
+ if (conversation?.input?.for === void 0 || sessions?.scope === void 0) return void 0;
4538
+ const id = currentSessionId();
4539
+ if (id === void 0) return void 0;
4540
+ try {
4541
+ return conversation.input.for(sessions.scope(id));
4542
+ } catch {
4543
+ return void 0;
4544
+ }
4545
+ }
4546
+ var placedText = "";
4547
+ function replaceBlock(text2) {
4548
+ const shell = draftShell();
4549
+ if (shell === void 0 || typeof shell.setDraft !== "function") return false;
4550
+ const snapshot = shell.state?.getSnapshot?.();
4551
+ const current = typeof snapshot?.draft === "string" ? String(snapshot.draft) : "";
4552
+ let rest = current;
4553
+ if (placedText !== "" && rest.includes(placedText)) rest = rest.split(placedText).join("");
4554
+ rest = rest.replace(/^[\r\n]+/, "").replace(/\s+$/, "");
4555
+ const next = text2 === "" ? rest : rest === "" ? text2 : rest + String.fromCharCode(10, 10) + text2;
4556
+ try {
4557
+ shell.setDraft(next);
4558
+ placedText = text2;
4559
+ return true;
4560
+ } catch {
4561
+ return false;
4562
+ }
4563
+ }
4564
+ var pendingBlock = "";
4565
+ var enterWatch = null;
4566
+ function setPendingBlock(text2) {
4567
+ pendingBlock = text2;
4568
+ installEnterCapture();
4569
+ }
4570
+ function findBlockElement(doc) {
4571
+ const all = Array.from(doc.querySelectorAll("div, p, span"));
4572
+ const hits = all.filter((node) => String(node.textContent ?? "").indexOf("Diagram annotations -") !== -1);
4573
+ let best;
4574
+ let bestArea = Number.POSITIVE_INFINITY;
4575
+ for (const node of hits) {
4576
+ const box = node.getBoundingClientRect?.();
4577
+ const area = box === void 0 || box === null ? Number.POSITIVE_INFINITY : box.width * box.height;
4578
+ if (area > 0 && area < bestArea) {
4579
+ bestArea = area;
4580
+ best = node;
4581
+ }
4582
+ }
4583
+ return best;
4584
+ }
4585
+ function insertChipAfter(doc, element, label) {
4586
+ try {
4587
+ const existing = Array.from(doc.querySelectorAll("[data-dme-chip]"));
4588
+ if (existing.some((old) => String(old.textContent ?? "") === label)) return;
4589
+ for (const old of existing) old.remove?.();
4590
+ const chip = doc.createElement("span");
4591
+ chip.setAttribute("data-dme-own", "1");
4592
+ chip.setAttribute("data-dme-chip", "1");
4593
+ chip.textContent = label;
4594
+ chip.style.cssText = "position:fixed;z-index:1140;display:inline-block;padding:1px 8px;border-radius:9px;font:11px system-ui;border:1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.35));color:var(--dsw-alias-label-secondary, #9aa0a6);background:var(--dsw-alias-bg-layer-1, rgba(30,30,32,0.92))";
4595
+ doc.body.appendChild(chip);
4596
+ chipAnchor = element;
4597
+ placeChip(doc);
4598
+ const timer2 = globalThis.setTimeout;
4599
+ if (typeof timer2 === "function") {
4600
+ for (const delay of [300, 900, 2e3, 3500]) timer2(() => {
4601
+ try {
4602
+ placeChip(doc);
4603
+ } catch {
4604
+ }
4605
+ }, delay);
4606
+ }
4607
+ const scope = globalThis;
4608
+ if (!chipScrollWatched && typeof scope.addEventListener === "function") {
4609
+ chipScrollWatched = true;
4610
+ scope.addEventListener("scroll", () => {
4611
+ try {
4612
+ const live = doc.querySelector("[data-dme-chip]");
4613
+ const anchor2 = chipAnchor;
4614
+ const box2 = anchor2?.getBoundingClientRect?.();
4615
+ if (live === null || box2 === void 0 || box2 === null) return;
4616
+ live.style.setProperty("left", String(Math.round(box2.left)) + "px");
4617
+ live.style.setProperty("top", String(Math.round(box2.bottom) + 4) + "px");
4618
+ } catch {
4619
+ }
4620
+ });
4621
+ }
4622
+ } catch {
4623
+ }
4624
+ }
4625
+ function placeChip(doc) {
4626
+ const chip = doc?.querySelector?.("[data-dme-chip]");
4627
+ if (chip === null || chip === void 0) return;
4628
+ if (chipAnchor === void 0 || chipAnchor.isConnected === false) chipAnchor = findBlockElement(doc);
4629
+ const anchor2 = chipAnchor;
4630
+ if (anchor2 === void 0) return;
4631
+ const box = anchor2.getBoundingClientRect?.();
4632
+ if (box === void 0 || box === null) return;
4633
+ if (box.width === 0) {
4634
+ return;
4635
+ }
4636
+ const view = globalThis;
4637
+ const outside = box.bottom < 0 || box.top > Number(view.innerHeight ?? 0);
4638
+ chip.style.setProperty("display", outside ? "none" : "inline-block");
4639
+ if (outside) return;
4640
+ chip.style.setProperty("left", String(Math.max(8, Math.round(box.left))) + "px");
4641
+ chip.style.setProperty("top", String(Math.max(8, Math.round(box.bottom) + 4)) + "px");
4642
+ }
4643
+ function attachBelowBubble(label, typed) {
4644
+ const doc = globalThis.document;
4645
+ const timer = globalThis.setTimeout;
4646
+ if (doc === void 0 || typeof timer !== "function") return;
4647
+ const wanted = typed.trim();
4648
+ let tries = 0;
4649
+ const attempt = () => {
4650
+ tries += 1;
4651
+ try {
4652
+ const all = Array.from(doc.querySelectorAll("div, p, span"));
4653
+ let hit;
4654
+ if (wanted !== "") {
4655
+ const hits = all.filter((node) => String(node.textContent ?? "").trim().startsWith(wanted) && node.parentElement);
4656
+ hit = hits[hits.length - 1];
4657
+ } else {
4658
+ hit = findBlockElement(doc);
4659
+ }
4660
+ chipAttempt = hit === void 0 ? "not found, try " + String(tries) : "found";
4661
+ if (hit !== void 0) {
4662
+ insertChipAfter(doc, hit, label);
4663
+ return;
4664
+ }
4665
+ } catch {
4666
+ }
4667
+ if (tries < 20) timer(attempt, 500);
4668
+ };
4669
+ timer(attempt, 300);
4670
+ }
4671
+ function attachSentChip(label) {
4672
+ const doc = globalThis.document;
4673
+ const timer = globalThis.setTimeout;
4674
+ if (doc === void 0 || typeof timer !== "function") return;
4675
+ let tries = 0;
4676
+ const attempt = () => {
4677
+ tries += 1;
4678
+ const anchor2 = doc.querySelector?.("[data-annotation-chip]");
4679
+ const bubble = anchor2?.parentElement;
4680
+ const row = bubble?.parentElement;
4681
+ const made = doc.createElement?.("span");
4682
+ if (made !== void 0 && bubble !== void 0 && bubble !== null && row !== void 0 && row !== null) {
4683
+ made.textContent = label;
4684
+ made.setAttribute?.("data-dme-own", "1");
4685
+ made.style.cssText = "display:inline-block;margin:4px 8px 0;padding:1px 8px;border-radius:9px;font:11px system-ui;border:1px solid var(--dsw-alias-border-l1, rgba(127,127,127,0.35));color:var(--dsw-alias-label-secondary, #9aa0a6)";
4686
+ try {
4687
+ row.insertBefore(made, bubble.nextSibling);
4688
+ return;
4689
+ } catch {
4690
+ }
4691
+ }
4692
+ if (tries < 8) timer(attempt, 250);
4693
+ };
4694
+ timer(attempt, 300);
4695
+ }
4696
+ var dockReact;
4697
+ var dockEl;
4698
+ var dockRetries = 0;
4699
+ var chipAttempt = "not attempted";
4700
+ var chipAnchor;
4701
+ var chipScrollWatched = false;
4702
+ var dockReceipt = "";
4703
+ var receiptTimer;
4704
+ function publishDockReceipt(text2) {
4705
+ dockReceipt = text2;
4706
+ const scope = globalThis;
4707
+ if (scope.clearTimeout !== void 0) scope.clearTimeout(receiptTimer);
4708
+ receiptTimer = scope.setTimeout?.(() => {
4709
+ dockReceipt = "";
4710
+ try {
4711
+ scheduleDock();
4712
+ } catch {
4713
+ }
4714
+ }, 8e3);
4715
+ }
4716
+ var dockObserver;
4717
+ var dockFrame;
4718
+ function ensureDock() {
4719
+ if (dockEl !== void 0 && dockEl.isConnected !== false) return dockEl;
4720
+ const doc = globalThis.document;
4721
+ if (doc?.body === void 0 || typeof doc.createElement !== "function") return void 0;
4722
+ for (const old of Array.from(doc.querySelectorAll("[data-dme-dock]"))) old.remove?.();
4723
+ const el = doc.createElement("div");
4724
+ el.setAttribute("data-dme-dock", "1");
4725
+ el.setAttribute("data-dme-own", "1");
4726
+ el.style.cssText = "position:fixed;z-index:1140;display:none;align-items:center;gap:6px;padding:4px 8px;border-radius:11px;border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35));background:var(--dsw-alias-bg-layer-1, #2c2c2e);box-shadow:0 8px 24px rgba(0,0,0,0.24);font:11px var(--dsw-font-family, system-ui);color:var(--dsw-alias-label-primary, #eee)";
4727
+ doc.body.appendChild(el);
4728
+ dockEl = el;
4729
+ return el;
4730
+ }
4731
+ function scheduleDock() {
4732
+ const raf = globalThis.requestAnimationFrame;
4733
+ if (dockFrame !== void 0 || typeof raf !== "function") {
4734
+ updateDockChip();
4735
+ return;
4736
+ }
4737
+ dockFrame = raf(() => {
4738
+ dockFrame = void 0;
4739
+ updateDockChip();
4740
+ });
4741
+ }
4742
+ function observeDockAnchor() {
4743
+ const doc = globalThis.document;
4744
+ const card = doc?.querySelector?.("[data-composer-card]");
4745
+ if (card === null || card === void 0) return;
4746
+ const Observer = globalThis.ResizeObserver;
4747
+ if (Observer === void 0) return;
4748
+ try {
4749
+ dockObserver?.disconnect();
4750
+ const fresh = new Observer(() => {
4751
+ scheduleDock();
4752
+ });
4753
+ fresh.observe(card);
4754
+ dockObserver = fresh;
4755
+ } catch {
4756
+ dockObserver = void 0;
4757
+ }
4758
+ }
4759
+ function updateDockChip() {
4760
+ const state = dockReact;
4761
+ const el = ensureDock();
4762
+ if (state === void 0 || el === void 0) return;
4763
+ const doc = globalThis.document;
4764
+ if (state.count === 0) {
4765
+ if (dockReceipt === "") {
4766
+ el.style.display = "none";
4767
+ return;
4768
+ }
4769
+ el.textContent = "";
4770
+ const note = doc.createElement("span");
4771
+ note.textContent = dockReceipt;
4772
+ note.style.cssText = "opacity:.75";
4773
+ el.appendChild(note);
4774
+ el.style.display = "flex";
4775
+ const cardRect2 = doc.querySelector("[data-composer-card]")?.getBoundingClientRect?.();
4776
+ if (cardRect2 === void 0) {
4777
+ el.style.left = "16px";
4778
+ el.style.top = "16px";
4779
+ return;
4780
+ }
4781
+ const w = Number(el.offsetWidth) || 100;
4782
+ const h = Number(el.offsetHeight) || 24;
4783
+ el.style.left = Math.max(8, Math.min(cardRect2.left, Number(globalThis.innerWidth ?? 0) - w - 8)) + "px";
4784
+ el.style.top = Math.max(8, cardRect2.top - h - 8) + "px";
4785
+ return;
4786
+ }
4787
+ const create = (tag) => doc.createElement(tag);
4788
+ el.textContent = "";
4789
+ const toggle = create("button");
4790
+ toggle.textContent = t("pending") + " \xD7" + String(state.count);
4791
+ toggle.style.cssText = "font:11px inherit;padding:1px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.35));background:transparent;color:inherit;cursor:pointer";
4792
+ toggle.addEventListener("click", () => state.handlers.onToggle());
4793
+ el.appendChild(toggle);
4794
+ const send = create("button");
4795
+ send.textContent = t("send");
4796
+ send.style.cssText = "font:11px inherit;padding:1px 8px;border-radius:6px;border:1px solid var(--dsw-alias-brand-primary,#2563eb);background:var(--dsw-alias-brand-primary,#2563eb);color:#fff;cursor:pointer";
4797
+ send.addEventListener("click", () => state.handlers.onSend());
4798
+ el.appendChild(send);
4799
+ if (state.open && state.items.length > 0) {
4800
+ for (const [index, item] of state.items.entries()) {
4801
+ const line = create("span");
4802
+ line.textContent = item.nodeId + ":" + (item.note === "" ? t("markedOnly") : item.note);
4803
+ line.title = "\u70B9\u51FB\u5220\u9664";
4804
+ line.style.cssText = "cursor:pointer;opacity:.85";
4805
+ line.addEventListener("click", () => state.handlers.onRemove(index));
4806
+ el.appendChild(line);
4807
+ }
4808
+ }
4809
+ el.style.display = "flex";
4810
+ observeDockAnchor();
4811
+ const surface = findComposer(doc) ?? doc.querySelector('textarea, [contenteditable="true"]');
4812
+ const rect = surface?.getBoundingClientRect?.();
4813
+ const cardEl = doc.querySelector("[data-composer-card]") ?? null;
4814
+ const seatEl = cardEl?.closest?.("[data-composer-seat]") ?? null;
4815
+ const cardRect = cardEl?.getBoundingClientRect?.() ?? null;
4816
+ void surface;
4817
+ const width = Number(el.offsetWidth) || 100;
4818
+ const height = Number(el.offsetHeight) || 26;
4819
+ if (rect === void 0 && cardRect === null) {
4820
+ el.style.left = "16px";
4821
+ el.style.top = "16px";
4822
+ return;
4823
+ }
4824
+ const view = globalThis;
4825
+ const anchor2 = cardRect ?? rect;
4826
+ const seatTop = seatEl?.getBoundingClientRect?.()?.top;
4827
+ const topEdge = Math.min(anchor2.top, typeof seatTop === "number" ? seatTop : anchor2.top);
4828
+ const left = Math.max(8, Math.min(anchor2.left, Number(view.innerWidth ?? 0) - width - 8));
4829
+ el.style.left = left + "px";
4830
+ el.style.top = Math.max(8, topEdge - height - 6) + "px";
4831
+ observeDivider(doc);
4832
+ observeSurface(doc, surface);
4833
+ const scope2 = globalThis;
4834
+ if (left <= 8 && dockRetries < 12 && typeof scope2.setTimeout === "function") {
4835
+ dockRetries += 1;
4836
+ scope2.setTimeout(() => {
4837
+ try {
4838
+ updateDockChip();
4839
+ } catch {
4840
+ }
4841
+ }, 250);
4842
+ } else if (left > 8) {
4843
+ dockRetries = 0;
4844
+ }
4845
+ }
4846
+ function updateDockLayer(items, open, handlers) {
4847
+ dockReact = { count: items.length, open, items, handlers };
4848
+ bumpDock();
4849
+ if (dockSlotMounted) return;
4850
+ try {
4851
+ scheduleDock();
4852
+ } catch {
4853
+ }
4854
+ }
4855
+ var dockSlotMounted = false;
4856
+ var dockVersion = 0;
4857
+ var dockSubs = /* @__PURE__ */ new Set();
4858
+ function bumpDock() {
4859
+ dockVersion += 1;
4860
+ for (const listener of Array.from(dockSubs)) {
4861
+ try {
4862
+ listener();
4863
+ } catch {
4864
+ }
4865
+ }
4866
+ }
4867
+ function dockSlotView(React) {
4868
+ return function DockSlot() {
4869
+ const [, setVersion] = (React.useState ?? ((initial) => [initial, () => {
4870
+ }]))(0);
4871
+ React.useEffect?.(() => {
4872
+ dockSlotMounted = true;
4873
+ const listener = () => setVersion(dockVersion);
4874
+ dockSubs.add(listener);
4875
+ return () => {
4876
+ dockSubs.delete(listener);
4877
+ dockSlotMounted = false;
4878
+ };
4879
+ }, []);
4880
+ const [pad, setPad] = (React.useState ?? ((initial) => [initial, () => {
4881
+ }]))({ left: 0, right: 0 });
4882
+ const [diag, setDiag] = (React.useState ?? ((initial) => [initial, () => {
4883
+ }]))("");
4884
+ React.useEffect?.(() => {
4885
+ const doc = globalThis.document;
4886
+ const timer = globalThis.setTimeout;
4887
+ const measure2 = () => {
4888
+ try {
4889
+ const slot = doc?.querySelector?.("[data-dme-dock-slot]");
4890
+ const card = findComposer(doc) ?? doc?.querySelector?.('textarea, [contenteditable="true"]');
4891
+ if (slot === null || slot === void 0 || card === null || card === void 0) return;
4892
+ const s = slot.getBoundingClientRect();
4893
+ const c = card.getBoundingClientRect();
4894
+ const next = { left: Math.max(0, Math.round(c.left - s.left)), right: Math.max(0, Math.round(s.right - c.right)) };
4895
+ setPad(next);
4896
+ setDiag(" L" + String(next.left) + " R" + String(next.right) + " s" + String(Math.round(s.left)) + " c" + String(Math.round(c.left)) + " w" + String(Math.round(c.width)));
4897
+ } catch {
4898
+ }
4899
+ };
4900
+ if (typeof timer === "function") {
4901
+ for (const delay of [0, 100, 250, 500, 900, 1500, 2400]) timer(measure2, delay);
4902
+ }
4903
+ const scope = globalThis;
4904
+ scope.addEventListener?.("resize", measure2);
4905
+ const Observer = globalThis.ResizeObserver;
4906
+ let watcher;
4907
+ if (Observer !== void 0) {
4908
+ try {
4909
+ watcher = new Observer(measure2);
4910
+ watcher.observe(doc?.body);
4911
+ } catch {
4912
+ watcher = void 0;
4913
+ }
4914
+ }
4915
+ return () => {
4916
+ watcher?.disconnect();
4917
+ scope.removeEventListener?.("resize", measure2);
4918
+ };
4919
+ }, []);
4920
+ const state = dockReact;
4921
+ if (state === void 0 || state.count === 0) return null;
4922
+ let livePad = pad;
4923
+ try {
4924
+ const doc = globalThis.document;
4925
+ const slotEl = doc?.querySelector?.("[data-dme-dock-slot]");
4926
+ const surfaceEl = findComposer(doc) ?? doc?.querySelector?.('textarea, [contenteditable="true"]');
4927
+ const sr = slotEl?.getBoundingClientRect?.();
4928
+ const cr = surfaceEl?.getBoundingClientRect?.();
4929
+ if (sr !== void 0 && sr !== null && cr !== void 0 && cr !== null) {
4930
+ livePad = { left: Math.max(0, Math.round(cr.left - sr.left)), right: Math.max(0, Math.round(sr.right - cr.right)) };
4931
+ }
4932
+ } catch {
4933
+ }
4934
+ return React.createElement(
4935
+ "div",
4936
+ {
4937
+ "data-dme-own": "1",
4938
+ "data-dme-dock-slot": "1",
4939
+ style: { display: "flex", alignItems: "center", justifyContent: "flex-start", gap: "6px", paddingTop: "2px", paddingBottom: "2px", paddingLeft: String(livePad.left) + "px", paddingRight: String(livePad.right) + "px", font: "11px system-ui, sans-serif", color: "inherit" }
4940
+ },
4941
+ React.createElement("button", {
4942
+ type: "button",
4943
+ onClick: () => state.handlers.onToggle(),
4944
+ style: { font: "inherit", padding: "1px 8px", borderRadius: "9px", border: "1px solid " + TOKEN.border, background: "transparent", color: "inherit", cursor: "pointer" }
4945
+ }, t("pending") + " \xD7" + String(state.count) + " L" + String(livePad.left) + diag),
4946
+ React.createElement("button", {
4947
+ type: "button",
4948
+ onClick: () => state.handlers.onSend(),
4949
+ style: { font: "inherit", padding: "1px 8px", borderRadius: "9px", border: "1px solid " + TOKEN.border, background: "transparent", color: "inherit", cursor: "pointer" }
4950
+ }, t("send"))
4951
+ );
4952
+ };
4953
+ }
4954
+ var dividerObserved;
4955
+ var surfaceObserved;
4956
+ var surfaceInputWatched = false;
4957
+ function observeSurface(doc, surface) {
4958
+ try {
4959
+ if (surface === void 0 || surface === null || surfaceObserved === surface) return;
4960
+ surfaceObserved = surface;
4961
+ const targets = [surface, surface?.parentElement].filter((node) => node !== null && node !== void 0);
4962
+ const Observer = globalThis.ResizeObserver;
4963
+ if (Observer !== void 0) {
4964
+ const watcher = new Observer(() => {
4965
+ scheduleDock();
4966
+ });
4967
+ for (const target of targets) {
4968
+ try {
4969
+ watcher.observe(target);
4970
+ } catch {
4971
+ }
4972
+ }
4973
+ }
4974
+ const scope = globalThis;
4975
+ if (typeof scope.addEventListener === "function" && !surfaceInputWatched) {
4976
+ surfaceInputWatched = true;
4977
+ scope.addEventListener("input", () => {
4978
+ scheduleDock();
4979
+ scope.setTimeout?.(() => {
4980
+ scheduleDock();
4981
+ }, 120);
4982
+ scope.setTimeout?.(() => {
4983
+ scheduleDock();
4984
+ }, 400);
4985
+ });
4986
+ scope.addEventListener("paste", () => {
4987
+ scope.setTimeout?.(() => {
4988
+ scheduleDock();
4989
+ }, 60);
4990
+ });
4991
+ }
4992
+ } catch {
4993
+ }
4994
+ }
4995
+ function observeDivider(doc) {
4996
+ try {
4997
+ const handle = doc?.querySelector?.('[class*="widthHandle"]');
4998
+ if (handle === null || handle === void 0 || dividerObserved === handle) return;
4999
+ dividerObserved = handle;
5000
+ const Observer = globalThis.ResizeObserver;
5001
+ if (Observer !== void 0) {
5002
+ try {
5003
+ new Observer(() => {
5004
+ scheduleDock();
5005
+ }).observe(handle);
5006
+ } catch {
5007
+ }
5008
+ }
5009
+ const scope = globalThis;
5010
+ scope.addEventListener?.("pointerup", () => {
5011
+ scheduleDock();
5012
+ });
5013
+ } catch {
5014
+ }
5015
+ }
5016
+ function domReport() {
5017
+ const doc = globalThis.document;
5018
+ if (doc === void 0) return "no document";
5019
+ const lines = [];
5020
+ const rect = (el) => {
5021
+ const r = el?.getBoundingClientRect?.();
5022
+ return r === void 0 || r === null ? "absent" : [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)].join(",");
5023
+ };
5024
+ const probe = (name, selector) => {
5025
+ const el = doc.querySelector(selector);
5026
+ lines.push(name + " " + (el === null || el === void 0 ? "absent" : "l,t,w,h=" + rect(el)));
5027
+ };
5028
+ probe("[data-composer-card]", "[data-composer-card]");
5029
+ probe("[data-composer-seat]", "[data-composer-seat]");
5030
+ probe("textarea/contenteditable", 'textarea, [contenteditable="true"]');
5031
+ probe("[data-annotation-chip]", "[data-annotation-chip]");
5032
+ probe("[data-conversation-scroll]", "[data-conversation-scroll]");
5033
+ probe("widthHandle", '[class*="widthHandle"]');
5034
+ const dock = doc.querySelector("[data-dme-dock]");
5035
+ lines.push("dock " + (dock === null ? "absent" : "l,t,w,h=" + rect(dock) + " connected=" + String(dock.isConnected)));
5036
+ const chips = doc.querySelectorAll("[data-dme-chip]");
5037
+ lines.push("chips " + String(chips.length) + " " + Array.from(chips).map((chip) => rect(chip)).join(" | "));
5038
+ lines.push("pendingBlock " + String(pendingBlock === "" ? "empty" : pendingBlock.split(String.fromCharCode(10)).length + " lines"));
5039
+ lines.push("chipAttempt " + chipAttempt);
5040
+ lines.push("dockedCount " + String(dockReact?.count ?? 0) + " placedText " + String(placedText.length));
5041
+ lines.push("viewport " + String(globalThis.innerWidth) + "x" + String(globalThis.innerHeight));
5042
+ const report = lines.join(String.fromCharCode(10));
5043
+ try {
5044
+ globalThis.console?.log?.(report);
5045
+ } catch {
5046
+ }
5047
+ return report;
5048
+ }
5049
+ function installEnterCapture() {
5050
+ if (pendingBlock === "") return;
5051
+ const doc = globalThis.document;
5052
+ const composer = findComposer(doc);
5053
+ if (composer === void 0 || composer === null) return;
5054
+ if (enterWatch === composer) return;
5055
+ enterWatch = composer;
5056
+ const listen = composer.addEventListener;
5057
+ if (typeof listen !== "function") return;
5058
+ listen.call(composer, "keydown", (event) => {
5059
+ const key = event;
5060
+ if (key?.key !== "Enter" || key.shiftKey === true || key.isComposing === true) return;
5061
+ if (pendingBlock === "") return;
5062
+ const shell = draftShell();
5063
+ const draft = String(shell?.state?.getSnapshot?.()?.draft ?? "");
5064
+ const block = draft.trim() === "" ? withoutEmptyQuestion(pendingBlock) : pendingBlock;
5065
+ replaceBlock(block);
5066
+ const count = (block.match(/^Diagram note \[/gm) ?? []).length;
5067
+ const label = "\u56FE\u6CE8 \xD7" + String(count === 0 ? 1 : count);
5068
+ attachBelowBubble(label, draft);
5069
+ void attachSentChip;
5070
+ notifySent();
5071
+ pendingBlock = "";
5072
+ }, true);
5073
+ }
5074
+ function notifySent() {
5075
+ if (dockReact !== void 0) {
5076
+ const before = dockReact.count;
5077
+ dockReact = { count: 0, open: false, items: [], handlers: dockReact.handlers };
5078
+ if (before > 0) publishDockReceipt("\u56FE\u6CE8 \xD7" + String(before) + " \u5DF2\u53D1\u9001");
5079
+ try {
5080
+ scheduleDock();
5081
+ } catch {
5082
+ }
5083
+ }
5084
+ for (const listener of sentListeners.slice()) {
5085
+ try {
5086
+ listener();
5087
+ } catch {
5088
+ }
5089
+ }
5090
+ }
5091
+ function placeBlock(text2) {
5092
+ const shell = draftShell();
5093
+ const snapshot = shell?.state?.getSnapshot?.();
5094
+ const current = typeof snapshot?.draft === "string" ? String(snapshot.draft) : "";
5095
+ return replaceBlock(current.trim() === "" ? withoutEmptyQuestion(text2) : text2);
5096
+ }
5097
+ var sentListeners = [];
5098
+ var watchState = "idle";
5099
+ var lastSeenDraft = "";
5100
+ function onMessageSent(listener) {
5101
+ sentListeners.push(listener);
5102
+ watchDraft();
5103
+ return () => {
5104
+ sentListeners = sentListeners.filter((item) => item !== listener);
5105
+ };
5106
+ }
5107
+ function watchDraft() {
5108
+ if (watchState !== "idle") return;
5109
+ const shell = draftShell();
5110
+ const subscribe = shell?.state?.subscribe;
5111
+ if (typeof subscribe !== "function") {
5112
+ return;
5113
+ }
5114
+ watchState = "watching";
5115
+ try {
5116
+ lastSeenDraft = String(shell?.state?.getSnapshot?.()?.draft ?? "");
5117
+ subscribe(() => {
5118
+ const draft = String(shell?.state?.getSnapshot?.()?.draft ?? "");
5119
+ const before = lastSeenDraft;
5120
+ lastSeenDraft = draft;
5121
+ if (before.trim() === "" || draft.trim() !== "") return;
5122
+ if (placedText === "") return;
5123
+ placedText = "";
5124
+ for (const listener of sentListeners.slice()) {
5125
+ try {
5126
+ listener();
5127
+ } catch {
5128
+ }
5129
+ }
5130
+ });
5131
+ } catch {
5132
+ watchState = "unavailable";
5133
+ }
5134
+ }
5135
+ function submitDraft() {
5136
+ const shell = draftShell();
5137
+ if (shell === void 0 || typeof shell.submit !== "function") return false;
5138
+ try {
5139
+ shell.submit("queue");
5140
+ return true;
5141
+ } catch {
5142
+ return false;
5143
+ }
5144
+ }
3322
5145
  function viewFor(React, overrides = {}) {
3323
5146
  return (props) => {
5147
+ if (props.toolName === DRAWING_TOOL) return drawingCard(React, props, overrides);
3324
5148
  const diagram = viewModel({ phase: props.phase, toolName: props.toolName, block: props.block });
3325
5149
  const { canvas, colours } = paintWith(diagram, overrides, { mode: themeMode() });
3326
5150
  const written = toSvg(canvas, colours, diagram.kind === "none" ? diagram.caption : diagram.title + ": " + diagram.caption);
@@ -3360,41 +5184,113 @@ window.__ModuleLoader__.load({
3360
5184
  const [scheme, setScheme] = useState("default");
3361
5185
  const [fit, setFit] = useState(true);
3362
5186
  const [folded, setFolded] = useState([]);
5187
+ const [annotate, setAnnotate] = useState(false);
5188
+ const [picked, setPicked] = useState(null);
5189
+ const [anchor2, setAnchor] = useState(void 0);
5190
+ const [note, setNote] = useState("");
5191
+ const [status, setStatus] = useState("");
5192
+ const [pending, setPending] = useState([]);
5193
+ const [open, setOpen] = useState(false);
5194
+ React.useEffect?.(() => {
5195
+ try {
5196
+ return onMessageSent(() => {
5197
+ setPending([]);
5198
+ setStatus("");
5199
+ });
5200
+ } catch {
5201
+ return void 0;
5202
+ }
5203
+ }, []);
3363
5204
  const drawn = drawSample(React, overrides, source, fit ? 0 : zoom, scheme, folded);
5205
+ const document = globalThis.document;
5206
+ const caption = source.split(String.fromCharCode(10)).map((line) => line.trim()).find((line) => line !== "") ?? "";
5207
+ const sendAll = (list, submit) => {
5208
+ if (list.length === 0) return;
5209
+ const text2 = annotationBlock(list.map((item) => ({ ...item, caption })));
5210
+ void (async () => {
5211
+ if (placeBlock(text2)) {
5212
+ setStatus("\u5DF2\u653E\u5165\u8F93\u5165\u6846\u5E76\u63D0\u4EA4\uFF08" + String(list.length) + " \u6761\u56FE\u6CE8\uFF09");
5213
+ setPending([]);
5214
+ if (submit) submitDraft();
5215
+ return;
5216
+ }
5217
+ const said = prependBlock(document, list, caption);
5218
+ setStatus(said);
5219
+ if (!said.startsWith("\u5DF2")) return;
5220
+ setPending([]);
5221
+ if (submit) submitComposer(document);
5222
+ })();
5223
+ };
5224
+ const addDraft = () => {
5225
+ if (picked === null) return false;
5226
+ const merged = [...pending, { nodeId: picked.id, note }];
5227
+ setPending(merged);
5228
+ setNote("");
5229
+ setPicked(null);
5230
+ setStatus("\u5DF2\u653E\u5165\u8F93\u5165\u6846\uFF0C\u76F4\u63A5\u56DE\u8F66\u5373\u53EF");
5231
+ return true;
5232
+ };
5233
+ const addAndSend = () => {
5234
+ if (picked === null) return;
5235
+ const merged = [...pending, { nodeId: picked.id, note }];
5236
+ setNote("");
5237
+ setPicked(null);
5238
+ sendAll(merged, true);
5239
+ };
3364
5240
  const textarea = h("textarea", {
5241
+ "data-dme-own": "1",
3365
5242
  value: source,
3366
5243
  spellcheck: false,
3367
5244
  rows: Math.min(12, Math.max(4, String(source).split("\n").length)),
3368
- style: { width: "100%", font: "12px ui-monospace, SFMono-Regular, Menlo, monospace", padding: "6px", resize: "vertical" },
5245
+ style: { width: "100%", boxSizing: "border-box", font: "12px ui-monospace, SFMono-Regular, Menlo, monospace", padding: "6px", resize: "vertical", borderRadius: "6px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text },
3369
5246
  onInput: (event) => setSource(String(event?.target?.value ?? ""))
3370
5247
  });
5248
+ const controlStyle = { fontSize: "11px", lineHeight: "18px", padding: "1px 7px", borderRadius: "6px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text, cursor: "pointer" };
5249
+ const control = (label, onPress, title) => h("button", { type: "button", title, onClick: onPress, style: controlStyle }, label);
3371
5250
  const controls = h(
3372
5251
  "div",
3373
- { style: { display: "flex", gap: "6px", alignItems: "center", padding: "4px 0", flexWrap: "wrap" } },
3374
- h("button", { type: "button", onClick: () => {
3375
- setFit(false);
3376
- setZoom(Math.max(0.5, Math.round((Number(zoom) - 0.25) * 100) / 100));
3377
- } }, "\u2212"),
3378
- h("span", { style: { fontSize: "12px", opacity: 0.7 } }, fit ? "fit" : String(Math.round(Number(zoom) * 100)) + "%"),
3379
- h("button", { type: "button", onClick: () => {
3380
- setFit(false);
3381
- setZoom(Math.min(3, Math.round((Number(zoom) + 0.25) * 100) / 100));
3382
- } }, "+"),
3383
- h("button", { type: "button", onClick: () => {
3384
- setFit(false);
3385
- setZoom(1);
3386
- } }, "reset"),
3387
- h("button", { type: "button", title: "Scale to the pane width", onClick: () => setFit(!fit) }, fit ? "fit: on" : "fit: off"),
3388
- folded.length === 0 ? null : h("button", { type: "button", title: "Unfold every group", onClick: () => setFolded([]) }, "unfold " + String(folded.length)),
5252
+ { style: { display: "flex", flexDirection: "column", gap: "3px", padding: "2px 0" } },
3389
5253
  h(
3390
- "select",
3391
- {
3392
- value: scheme,
3393
- title: "Colour scheme",
3394
- style: { marginLeft: "auto", fontSize: "12px" },
3395
- onChange: (event) => setScheme(String(event?.target?.value ?? "default"))
3396
- },
3397
- ...schemeNames().map((name) => h("option", { value: name, selected: name === scheme }, name))
5254
+ "div",
5255
+ { style: { display: "flex", gap: "4px", alignItems: "center", flexWrap: "wrap" } },
5256
+ control("\u2212", () => {
5257
+ setFit(false);
5258
+ setZoom(Math.max(0.5, Math.round((Number(zoom) - 0.25) * 100) / 100));
5259
+ }, "\u7F29\u5C0F"),
5260
+ h("span", { style: { fontSize: "11px", color: TOKEN.dim, minWidth: "34px", textAlign: "center" } }, fit ? "fit" : String(Math.round(Number(zoom) * 100)) + "%"),
5261
+ control("+", () => {
5262
+ setFit(false);
5263
+ setZoom(Math.min(3, Math.round((Number(zoom) + 0.25) * 100) / 100));
5264
+ }, "\u653E\u5927"),
5265
+ control("100%", () => {
5266
+ setFit(false);
5267
+ setZoom(1);
5268
+ }, "\u5B9E\u9645\u5927\u5C0F"),
5269
+ control(fit ? t("fitOn") : t("fitOff"), () => setFit(!fit), t("fitOff")),
5270
+ folded.length === 0 ? null : control(t("unfold") + " " + String(folded.length), () => setFolded([]), "\u5C55\u5F00\u6240\u6709\u5206\u7EC4"),
5271
+ h(
5272
+ "select",
5273
+ {
5274
+ value: scheme,
5275
+ title: t("scheme"),
5276
+ style: { marginLeft: "auto", fontSize: "11px", padding: "1px 4px", borderRadius: "6px", border: "1px solid " + TOKEN.border, background: "transparent", color: TOKEN.text },
5277
+ onChange: (event) => setScheme(String(event?.target?.value ?? "default"))
5278
+ },
5279
+ ...schemeNames().map((name) => h("option", { value: name, selected: name === scheme }, name))
5280
+ )
5281
+ ),
5282
+ h(
5283
+ "div",
5284
+ { style: { display: "flex", gap: "6px", alignItems: "center" } },
5285
+ control(annotate ? t("noteOn") : t("note"), () => {
5286
+ setAnnotate(!annotate);
5287
+ setStatus("");
5288
+ if (annotate) {
5289
+ setPicked(null);
5290
+ setNote("");
5291
+ }
5292
+ }, t("noteHint")),
5293
+ h("span", { style: { fontSize: "11px", color: TOKEN.dim } }, annotate ? picked === null ? t("noteHint") : t("node") + " " + picked.id : t("noteHint"))
3398
5294
  )
3399
5295
  );
3400
5296
  return h(
@@ -3402,21 +5298,76 @@ window.__ModuleLoader__.load({
3402
5298
  { style: { display: "flex", flexDirection: "column", gap: "4px", height: "100%", boxSizing: "border-box", padding: "8px" } },
3403
5299
  textarea,
3404
5300
  controls,
3405
- // The drawing takes the rest of the pane and scrolls inside it, so an expanded tab uses its room.
5301
+ // The drawing takes the rest of the pane and scrolls inside it; the annotation surface floats above it,
5302
+ // so the picture keeps its size while the reader writes about it.
3406
5303
  h(
3407
5304
  "div",
3408
5305
  {
3409
- style: { flex: "1 1 auto", minHeight: "0", overflow: "auto" },
3410
- // A boundary carries its id in the markup, so one listener on the container is enough: no per-rect
3411
- // handler, and the handling survives a re-render that replaced the whole SVG.
3412
- onClick: (event) => {
3413
- const hit = event?.target?.closest?.("[data-group-id]");
3414
- const id = hit?.getAttribute?.("data-group-id");
3415
- if (typeof id !== "string" || id === "") return;
3416
- setFolded(folded.includes(id) ? folded.filter((entry) => entry !== id) : [...folded, id]);
3417
- }
5306
+ style: { position: "relative", flex: "1 1 auto", minHeight: "0", display: "flex", flexDirection: "column", gap: "2px" }
3418
5307
  },
3419
- drawn
5308
+ h(
5309
+ "div",
5310
+ {
5311
+ style: { flex: "1 1 auto", minHeight: "0", overflow: "auto" },
5312
+ // A boundary carries its id in the markup, so one listener on the container is enough: no per-rect
5313
+ // handler, and the handling survives a re-render that replaced the whole SVG.
5314
+ onClick: (event) => {
5315
+ if (annotate) {
5316
+ const node = event?.target?.closest?.("[data-node-id]");
5317
+ const marked = node?.getAttribute?.("data-node-id");
5318
+ if (typeof marked === "string" && marked !== "") {
5319
+ const pointer = event;
5320
+ const frame = pointer.currentTarget?.getBoundingClientRect?.();
5321
+ if (frame !== void 0 && typeof pointer.clientX === "number" && typeof pointer.clientY === "number") {
5322
+ setAnchor({
5323
+ left: Math.max(4, Math.min(pointer.clientX - frame.left + 12, frame.width - 270)),
5324
+ top: Math.max(4, Math.min(pointer.clientY - frame.top + 12, Math.max(4, frame.height - 160)))
5325
+ });
5326
+ } else {
5327
+ setAnchor({ left: 10, top: 10 });
5328
+ }
5329
+ setPicked({ id: marked });
5330
+ setStatus("");
5331
+ return;
5332
+ }
5333
+ if (note.trim() === "") {
5334
+ setPicked(null);
5335
+ setAnchor(void 0);
5336
+ }
5337
+ return;
5338
+ }
5339
+ const hit = event?.target?.closest?.("[data-group-id]");
5340
+ const id = hit?.getAttribute?.("data-group-id");
5341
+ if (typeof id !== "string" || id === "") return;
5342
+ setFolded(folded.includes(id) ? folded.filter((entry) => entry !== id) : [...folded, id]);
5343
+ }
5344
+ },
5345
+ drawn
5346
+ ),
5347
+ annotate ? annotationPanel(
5348
+ h,
5349
+ { picked, note, pending, status, anchor: anchor2, open },
5350
+ {
5351
+ onNote: setNote,
5352
+ onAdd: () => {
5353
+ addDraft();
5354
+ },
5355
+ onAddAndSend: addAndSend,
5356
+ onSend: () => sendAll(pending, true),
5357
+ onClear: () => {
5358
+ setPending([]);
5359
+ setStatus("");
5360
+ },
5361
+ onCancel: () => {
5362
+ setPicked(null);
5363
+ setNote("");
5364
+ setStatus("");
5365
+ },
5366
+ onToggle: () => setOpen(!open),
5367
+ onRemove: (index) => setPending(pending.filter((_, at) => at !== index))
5368
+ },
5369
+ { document }
5370
+ ) : null
3420
5371
  )
3421
5372
  );
3422
5373
  };
@@ -3480,10 +5431,13 @@ window.__ModuleLoader__.load({
3480
5431
  inject: ["slots"],
3481
5432
  mount: (ctx, options = {}) => mountWhiteboard(React, ctx, options),
3482
5433
  apply(ctx) {
5434
+ services = ctx;
5435
+ void startTranscriptDecoration;
3483
5436
  for (const name of TOOL_NAMES) {
3484
5437
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({ name: "tool.call.toolview", key: name }, viewFor(React)));
3485
5438
  }
3486
5439
  ctx.slots.inject("sidebar.right.pane.tab", () => mountWhiteboard(React, ctx));
5440
+ void dockSlotView;
3487
5441
  }
3488
5442
  };
3489
5443
  }