@retikz/core 0.1.0-alpha.4 → 0.1.0-alpha.5

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.
Files changed (193) hide show
  1. package/dist/es/compile/compile.d.ts +6 -13
  2. package/dist/es/compile/compile.d.ts.map +1 -1
  3. package/dist/es/compile/compile.js +4 -10
  4. package/dist/es/compile/node.d.ts +40 -59
  5. package/dist/es/compile/node.d.ts.map +1 -1
  6. package/dist/es/compile/node.js +51 -43
  7. package/dist/es/compile/parseTarget.d.ts +3 -8
  8. package/dist/es/compile/parseTarget.d.ts.map +1 -1
  9. package/dist/es/compile/parseTarget.js +7 -19
  10. package/dist/es/compile/path.d.ts +2 -19
  11. package/dist/es/compile/path.d.ts.map +1 -1
  12. package/dist/es/compile/path.js +232 -213
  13. package/dist/es/compile/position.d.ts +4 -8
  14. package/dist/es/compile/position.d.ts.map +1 -1
  15. package/dist/es/compile/position.js +10 -10
  16. package/dist/es/compile/text-metrics.d.ts +2 -4
  17. package/dist/es/compile/text-metrics.d.ts.map +1 -1
  18. package/dist/es/compile/view-box.d.ts +1 -4
  19. package/dist/es/compile/view-box.d.ts.map +1 -1
  20. package/dist/es/compile/view-box.js +1 -4
  21. package/dist/es/geometry/arc.d.ts +3 -28
  22. package/dist/es/geometry/arc.d.ts.map +1 -1
  23. package/dist/es/geometry/arc.js +4 -31
  24. package/dist/es/geometry/bend.d.ts +2 -13
  25. package/dist/es/geometry/bend.d.ts.map +1 -1
  26. package/dist/es/geometry/bend.js +2 -13
  27. package/dist/es/geometry/circle.d.ts +5 -13
  28. package/dist/es/geometry/circle.d.ts.map +1 -1
  29. package/dist/es/geometry/circle.js +1 -4
  30. package/dist/es/geometry/diamond.d.ts +9 -24
  31. package/dist/es/geometry/diamond.d.ts.map +1 -1
  32. package/dist/es/geometry/diamond.js +4 -14
  33. package/dist/es/geometry/ellipse.d.ts +9 -15
  34. package/dist/es/geometry/ellipse.d.ts.map +1 -1
  35. package/dist/es/geometry/ellipse.js +4 -8
  36. package/dist/es/geometry/point.d.ts +8 -9
  37. package/dist/es/geometry/point.d.ts.map +1 -1
  38. package/dist/es/geometry/point.js +8 -9
  39. package/dist/es/geometry/polar.d.ts +12 -21
  40. package/dist/es/geometry/polar.d.ts.map +1 -1
  41. package/dist/es/geometry/polar.js +7 -14
  42. package/dist/es/geometry/rect.d.ts +8 -32
  43. package/dist/es/geometry/rect.d.ts.map +1 -1
  44. package/dist/es/geometry/rect.js +7 -33
  45. package/dist/es/geometry/segment.d.ts +11 -18
  46. package/dist/es/geometry/segment.d.ts.map +1 -1
  47. package/dist/es/geometry/segment.js +9 -16
  48. package/dist/es/index.d.ts +6 -10
  49. package/dist/es/index.d.ts.map +1 -1
  50. package/dist/es/index.js +4 -3
  51. package/dist/es/ir/coordinate.d.ts +18 -10
  52. package/dist/es/ir/coordinate.d.ts.map +1 -1
  53. package/dist/es/ir/coordinate.js +6 -11
  54. package/dist/es/ir/node.d.ts +46 -65
  55. package/dist/es/ir/node.d.ts.map +1 -1
  56. package/dist/es/ir/node.js +15 -46
  57. package/dist/es/ir/path/arrow.d.ts +210 -12
  58. package/dist/es/ir/path/arrow.d.ts.map +1 -1
  59. package/dist/es/ir/path/arrow.js +39 -12
  60. package/dist/es/ir/path/path.d.ts +477 -153
  61. package/dist/es/ir/path/path.d.ts.map +1 -1
  62. package/dist/es/ir/path/path.js +2 -2
  63. package/dist/es/ir/path/step.d.ts +395 -223
  64. package/dist/es/ir/path/step.d.ts.map +1 -1
  65. package/dist/es/ir/path/step.js +13 -14
  66. package/dist/es/ir/path/target.d.ts +25 -16
  67. package/dist/es/ir/path/target.d.ts.map +1 -1
  68. package/dist/es/ir/path/target.js +8 -6
  69. package/dist/es/ir/position/at-position.d.ts +4 -11
  70. package/dist/es/ir/position/at-position.d.ts.map +1 -1
  71. package/dist/es/ir/position/at-position.js +2 -9
  72. package/dist/es/ir/position/index.d.ts +1 -0
  73. package/dist/es/ir/position/index.d.ts.map +1 -1
  74. package/dist/es/ir/position/offset-position.d.ts +14 -0
  75. package/dist/es/ir/position/offset-position.d.ts.map +1 -0
  76. package/dist/es/ir/position/offset-position.js +14 -0
  77. package/dist/es/ir/position/polar-position.d.ts +1 -4
  78. package/dist/es/ir/position/polar-position.d.ts.map +1 -1
  79. package/dist/es/ir/position/polar-position.js +1 -4
  80. package/dist/es/ir/scene.d.ts +1200 -386
  81. package/dist/es/ir/scene.d.ts.map +1 -1
  82. package/dist/es/parsers/parseTargetSugar.d.ts.map +1 -1
  83. package/dist/es/parsers/parseTargetSugar.js +6 -19
  84. package/dist/es/parsers/parseWay.d.ts +26 -118
  85. package/dist/es/parsers/parseWay.d.ts.map +1 -1
  86. package/dist/es/parsers/parseWay.js +19 -61
  87. package/dist/es/primitive/ellipse.d.ts +4 -14
  88. package/dist/es/primitive/ellipse.d.ts.map +1 -1
  89. package/dist/es/primitive/group.d.ts +21 -3
  90. package/dist/es/primitive/group.d.ts.map +1 -1
  91. package/dist/es/primitive/path.d.ts +67 -7
  92. package/dist/es/primitive/path.d.ts.map +1 -1
  93. package/dist/es/primitive/scene.d.ts +2 -4
  94. package/dist/es/primitive/scene.d.ts.map +1 -1
  95. package/dist/es/primitive/text.d.ts +9 -32
  96. package/dist/es/primitive/text.d.ts.map +1 -1
  97. package/dist/lib/compile/compile.cjs +4 -10
  98. package/dist/lib/compile/compile.d.ts +6 -13
  99. package/dist/lib/compile/compile.d.ts.map +1 -1
  100. package/dist/lib/compile/node.cjs +51 -43
  101. package/dist/lib/compile/node.d.ts +40 -59
  102. package/dist/lib/compile/node.d.ts.map +1 -1
  103. package/dist/lib/compile/parseTarget.cjs +7 -19
  104. package/dist/lib/compile/parseTarget.d.ts +3 -8
  105. package/dist/lib/compile/parseTarget.d.ts.map +1 -1
  106. package/dist/lib/compile/path.cjs +231 -212
  107. package/dist/lib/compile/path.d.ts +2 -19
  108. package/dist/lib/compile/path.d.ts.map +1 -1
  109. package/dist/lib/compile/position.cjs +10 -10
  110. package/dist/lib/compile/position.d.ts +4 -8
  111. package/dist/lib/compile/position.d.ts.map +1 -1
  112. package/dist/lib/compile/text-metrics.d.ts +2 -4
  113. package/dist/lib/compile/text-metrics.d.ts.map +1 -1
  114. package/dist/lib/compile/view-box.cjs +1 -4
  115. package/dist/lib/compile/view-box.d.ts +1 -4
  116. package/dist/lib/compile/view-box.d.ts.map +1 -1
  117. package/dist/lib/geometry/arc.cjs +3 -31
  118. package/dist/lib/geometry/arc.d.ts +3 -28
  119. package/dist/lib/geometry/arc.d.ts.map +1 -1
  120. package/dist/lib/geometry/bend.cjs +2 -13
  121. package/dist/lib/geometry/bend.d.ts +2 -13
  122. package/dist/lib/geometry/bend.d.ts.map +1 -1
  123. package/dist/lib/geometry/circle.cjs +1 -4
  124. package/dist/lib/geometry/circle.d.ts +5 -13
  125. package/dist/lib/geometry/circle.d.ts.map +1 -1
  126. package/dist/lib/geometry/diamond.cjs +4 -14
  127. package/dist/lib/geometry/diamond.d.ts +9 -24
  128. package/dist/lib/geometry/diamond.d.ts.map +1 -1
  129. package/dist/lib/geometry/ellipse.cjs +4 -8
  130. package/dist/lib/geometry/ellipse.d.ts +9 -15
  131. package/dist/lib/geometry/ellipse.d.ts.map +1 -1
  132. package/dist/lib/geometry/point.cjs +8 -9
  133. package/dist/lib/geometry/point.d.ts +8 -9
  134. package/dist/lib/geometry/point.d.ts.map +1 -1
  135. package/dist/lib/geometry/polar.cjs +7 -14
  136. package/dist/lib/geometry/polar.d.ts +12 -21
  137. package/dist/lib/geometry/polar.d.ts.map +1 -1
  138. package/dist/lib/geometry/rect.cjs +7 -33
  139. package/dist/lib/geometry/rect.d.ts +8 -32
  140. package/dist/lib/geometry/rect.d.ts.map +1 -1
  141. package/dist/lib/geometry/segment.cjs +9 -16
  142. package/dist/lib/geometry/segment.d.ts +11 -18
  143. package/dist/lib/geometry/segment.d.ts.map +1 -1
  144. package/dist/lib/index.cjs +9 -2
  145. package/dist/lib/index.d.ts +6 -10
  146. package/dist/lib/index.d.ts.map +1 -1
  147. package/dist/lib/ir/coordinate.cjs +6 -11
  148. package/dist/lib/ir/coordinate.d.ts +18 -10
  149. package/dist/lib/ir/coordinate.d.ts.map +1 -1
  150. package/dist/lib/ir/node.cjs +15 -46
  151. package/dist/lib/ir/node.d.ts +46 -65
  152. package/dist/lib/ir/node.d.ts.map +1 -1
  153. package/dist/lib/ir/path/arrow.cjs +43 -11
  154. package/dist/lib/ir/path/arrow.d.ts +210 -12
  155. package/dist/lib/ir/path/arrow.d.ts.map +1 -1
  156. package/dist/lib/ir/path/path.cjs +1 -1
  157. package/dist/lib/ir/path/path.d.ts +477 -153
  158. package/dist/lib/ir/path/path.d.ts.map +1 -1
  159. package/dist/lib/ir/path/step.cjs +13 -14
  160. package/dist/lib/ir/path/step.d.ts +395 -223
  161. package/dist/lib/ir/path/step.d.ts.map +1 -1
  162. package/dist/lib/ir/path/target.cjs +9 -7
  163. package/dist/lib/ir/path/target.d.ts +25 -16
  164. package/dist/lib/ir/path/target.d.ts.map +1 -1
  165. package/dist/lib/ir/position/at-position.cjs +2 -9
  166. package/dist/lib/ir/position/at-position.d.ts +4 -11
  167. package/dist/lib/ir/position/at-position.d.ts.map +1 -1
  168. package/dist/lib/ir/position/index.d.ts +1 -0
  169. package/dist/lib/ir/position/index.d.ts.map +1 -1
  170. package/dist/lib/ir/position/offset-position.cjs +14 -0
  171. package/dist/lib/ir/position/offset-position.d.ts +14 -0
  172. package/dist/lib/ir/position/offset-position.d.ts.map +1 -0
  173. package/dist/lib/ir/position/polar-position.cjs +1 -4
  174. package/dist/lib/ir/position/polar-position.d.ts +1 -4
  175. package/dist/lib/ir/position/polar-position.d.ts.map +1 -1
  176. package/dist/lib/ir/scene.d.ts +1200 -386
  177. package/dist/lib/ir/scene.d.ts.map +1 -1
  178. package/dist/lib/parsers/parseTargetSugar.cjs +6 -19
  179. package/dist/lib/parsers/parseTargetSugar.d.ts.map +1 -1
  180. package/dist/lib/parsers/parseWay.cjs +19 -61
  181. package/dist/lib/parsers/parseWay.d.ts +26 -118
  182. package/dist/lib/parsers/parseWay.d.ts.map +1 -1
  183. package/dist/lib/primitive/ellipse.d.ts +4 -14
  184. package/dist/lib/primitive/ellipse.d.ts.map +1 -1
  185. package/dist/lib/primitive/group.d.ts +21 -3
  186. package/dist/lib/primitive/group.d.ts.map +1 -1
  187. package/dist/lib/primitive/path.d.ts +67 -7
  188. package/dist/lib/primitive/path.d.ts.map +1 -1
  189. package/dist/lib/primitive/scene.d.ts +2 -4
  190. package/dist/lib/primitive/scene.d.ts.map +1 -1
  191. package/dist/lib/primitive/text.d.ts +9 -32
  192. package/dist/lib/primitive/text.d.ts.map +1 -1
  193. package/package.json +1 -1
@@ -1,52 +1,72 @@
1
+ import { HOLLOW_ARROW_SHAPES } from "../ir/path/arrow.js";
1
2
  import { resolvePosition } from "./position.js";
2
3
  import { anchorOf, angleBoundaryOf, boundaryPointOf } from "./node.js";
3
- import { arcBoundingPoints, arcEndPoint, arcSvgFlags } from "../geometry/arc.js";
4
+ import { arcBoundingPoints, arcEndPoint } from "../geometry/arc.js";
4
5
  import { bendControlPoints } from "../geometry/bend.js";
5
6
  import { arcSegmentSample, circleSegmentSample, cubicSegmentSample, ellipseSegmentSample, foldSegmentSample, lineSegmentSample, quadSegmentSample } from "../geometry/segment.js";
6
7
  import { parseNodeRef } from "./parseTarget.js";
7
8
  import { fallbackMeasurer } from "./text-metrics.js";
8
9
  //#region src/compile/path.ts
9
10
  /**
10
- * IR path-level `arrow` + `arrowShape` 映射到 PathPrim 的
11
- * arrowStart / arrowEnd(值就是 shape 名)。`arrowShape` 省略时默认 'normal'。
11
+ * 端点级 spec:顶层默认 ⊕ end-side override(逐字段 merge)
12
+ * @description 缺省字段继承顶层(不是"完全替换");空心 shape fill 字段被丢(silent no-op)
12
13
  */
13
- var arrowMarkers = (arrow, shape = "normal") => {
14
+ var resolveArrowEndSpec = (topLevel, endSide) => {
15
+ const baseShape = endSide?.shape ?? topLevel.shape ?? "normal";
16
+ const out = { shape: baseShape };
17
+ const scale = endSide?.scale ?? topLevel.scale;
18
+ if (scale !== void 0) out.scale = scale;
19
+ const length = endSide?.length ?? topLevel.length;
20
+ if (length !== void 0) out.length = length;
21
+ const width = endSide?.width ?? topLevel.width;
22
+ if (width !== void 0) out.width = width;
23
+ const color = endSide?.color ?? topLevel.color;
24
+ if (color !== void 0) out.color = color;
25
+ const opacity = endSide?.opacity ?? topLevel.opacity;
26
+ if (opacity !== void 0) out.opacity = opacity;
27
+ const lineWidth = endSide?.lineWidth ?? topLevel.lineWidth;
28
+ if (lineWidth !== void 0) out.lineWidth = lineWidth;
29
+ if (!HOLLOW_ARROW_SHAPES.has(baseShape)) {
30
+ const fill = endSide?.fill ?? topLevel.fill;
31
+ if (fill !== void 0) out.fill = fill;
32
+ }
33
+ return out;
34
+ };
35
+ /** IR path-level `arrow` + `arrowDetail` → PathPrim 起末视觉规格 */
36
+ var arrowMarkers = (arrow, detail) => {
37
+ if (!arrow || arrow === "none") return {};
38
+ const top = detail ?? {};
39
+ const startSpec = resolveArrowEndSpec(top, top.start);
40
+ const endSpec = resolveArrowEndSpec(top, top.end);
14
41
  switch (arrow) {
15
- case "->": return { arrowEnd: shape };
16
- case "<-": return { arrowStart: shape };
42
+ case "->": return { arrowEnd: endSpec };
43
+ case "<-": return { arrowStart: startSpec };
17
44
  case "<->": return {
18
- arrowStart: shape,
19
- arrowEnd: shape
45
+ arrowStart: startSpec,
46
+ arrowEnd: endSpec
20
47
  };
21
- default: return {};
22
48
  }
23
49
  };
24
50
  /**
25
- * arrow shape 决定线段需要从端点向内"缩短"多少(单位:strokeWidth 倍)。
51
+ * 端点级 shrink(strokeWidth 倍):line 末端朝起点缩这么多,让 marker apex 落回原 target
52
+ * @description 不分实心/空心:所有 shape 都让 line 端点接在箭头尾部、apex 顶端仍贴原 target。低 opacity 下不会再透出 line。viewBox=10,shrink = (apex.x - refX) × length × scale / 10(strokeWidth 倍)。
26
53
  *
27
- * 用途:避免 hollow shape(如 `open` / `openDiamond` / `openCircle`)的
28
- * 空心内部被 path 描边穿过——把线末端退到形状背面位置,marker apex
29
- * 才能正好落在原始端点上。
30
- *
31
- * 这个值与 marker 几何配套,必须与 react/render/arrowMarkers.tsx
32
- * 各 shape 的 refX / 形状定义保持一致:
33
- * shrink = (apexX - refX) × markerWidth / viewBoxWidth
34
- * open: apexX=9, refX=1, scale=6/10 → 8 × 0.6 = 4.8
35
- * openDiamond: apexX=9, refX=1, scale=6/10 → 8 × 0.6 = 4.8
36
- * openCircle: apexX=10, refX=0, scale=6/10 → 10 × 0.6 = 6
37
- * 实心 shape(normal / stealth / diamond / circle)apex / 边缘已贴 refX=10,
38
- * line 被 fill 覆盖看不见,shrink=0。
54
+ * 几何对齐(必须与 react/render/arrowMarkers.tsx renderInner refX 一致):
55
+ * - `normal` / `diamond` / `circle`:apex 在 viewBox x=10、back 外缘 x=0 → refX=0,shrink = length × scale
56
+ * - `stealth`:apex x=10、V tip x=3(line 嵌进 V 凹口)→ refX=3,shrink = 0.7 × length × scale
57
+ * - `open` / `openDiamond`:apex x=9、back stroke 外缘 x = 1 - lineWidth/2 → refX = 1 - lineWidth/2,shrink = (8 + lineWidth/2) × length × scale / 10
58
+ * - `openCircle`:apex 外缘右 x ≈ 10、back 外缘左 x = 0.75 - lineWidth/2 → refX = 0.75 - lineWidth/2,shrink ≈ length × scale
39
59
  */
40
- var SHRINK_FOR_SHAPE = {
41
- normal: 0,
42
- open: 4.8,
43
- stealth: 0,
44
- diamond: 0,
45
- openDiamond: 4.8,
46
- circle: 0,
47
- openCircle: 6
60
+ var computeShrink = (spec) => {
61
+ const length = (spec.length ?? 6) * (spec.scale ?? 1);
62
+ if (HOLLOW_ARROW_SHAPES.has(spec.shape)) {
63
+ if (spec.shape === "openCircle") return length;
64
+ return (8 + (spec.lineWidth ?? 1.5) / 2) * length / 10;
65
+ }
66
+ if (spec.shape === "stealth") return 7 * length / 10;
67
+ return length;
48
68
  };
49
- /** 把点 p 朝 target 方向移动 dist 个 path 单位 */
69
+ /** p 朝 target 方向移动 dist */
50
70
  var shiftToward = (p, target, dist) => {
51
71
  const dx = target[0] - p[0];
52
72
  const dy = target[1] - p[1];
@@ -55,15 +75,8 @@ var shiftToward = (p, target, dist) => {
55
75
  return [p[0] + dx / len * dist, p[1] + dy / len * dist];
56
76
  };
57
77
  /**
58
- * 求一个 step.to 的"参考点"——给段内 boundary clip 算方向 / 折角 corner 用。
59
- *
60
- * 三态字符串语法(ADR-0004):
61
- * - `'A'`(auto):节点中心;邻居端点 clip 时按"A 中心 → toward"射线求边界
62
- * - `'A.<anchor>'`:命名 anchor 位置(位置即 endpoint,refPoint 也是它)
63
- * - `'A.<deg>'`:节点 deg 方向上的视觉边界点(位置即 endpoint)
64
- *
65
- * 后两种"显式锚点"模式下 refPoint = endpoint(位置不随邻居变),auto 模式
66
- * 下 refPoint 仍是中心。直接坐标 / 极坐标:解析后的笛卡尔。
78
+ * step.to 的参考点(给 boundary clip 算方向 / 折角 corner 用)
79
+ * @description 三态:`'A'`(auto) 节点中心;`'A.<anchor>'`/`'A.<deg>'` 显式锚点 refPoint=endpoint 位置不随邻居变。直接坐标/极坐标解析为笛卡尔
67
80
  */
68
81
  var refPointOfTarget = (target, nodeIndex) => {
69
82
  if (typeof target === "string") {
@@ -76,22 +89,14 @@ var refPointOfTarget = (target, nodeIndex) => {
76
89
  case "angle": return angleBoundaryOf(node, ref.angle);
77
90
  }
78
91
  }
79
- if (typeof target === "object" && !Array.isArray(target) && ("rel" in target || "relAccumulate" in target)) return null;
92
+ if (typeof target === "object" && !Array.isArray(target) && ("relative" in target || "relativeAccumulate" in target)) return null;
80
93
  return resolvePosition(target, nodeIndex);
81
94
  };
82
- /**
83
- * 折角中间点:基于"参考点"(节点中心或直接坐标)算直角拐点。
84
- * `-|` corner = (curr.x, prev.y);`|-` corner = (prev.x, curr.y)。
85
- */
95
+ /** 折角中间点:`-|` → (curr.x, prev.y);`|-` → (prev.x, curr.y) */
86
96
  var cornerOf = (prev, curr, via) => via === "-|" ? [curr[0], prev[1]] : [prev[0], curr[1]];
87
97
  /**
88
- * step.to 在给定方向 `toward` 上算出"实际绘制端点":
89
- * - 节点 ref auto(`'A'`):按 shape 多态走 boundaryPointOf——外扩 margin
90
- * 求边界与"中心→toward"射线交点
91
- * - 节点 ref 命名 anchor(`'A.north'`)/ 角度(`'A.30'`):位置已被解析定死,
92
- * 直接返回,**不再受 toward 影响**
93
- * - 直接坐标 / 极坐标:解析后直接返回(不做 clip)
94
- * 解析失败返回 null。
98
+ * toward 方向算 step.to 的实际绘制端点
99
+ * @description 节点 auto `'A'`:按 shape boundaryPointOf 求中心→toward 射线交点;命名 anchor/角度:位置已定不受 toward 影响;直接坐标/极坐标:解析后返回;失败返回 null
95
100
  */
96
101
  var clipForTarget = (target, toward, nodeIndex) => {
97
102
  if (typeof target === "string") {
@@ -104,24 +109,14 @@ var clipForTarget = (target, toward, nodeIndex) => {
104
109
  case "angle": return angleBoundaryOf(node, ref.angle);
105
110
  }
106
111
  }
107
- if (typeof target === "object" && !Array.isArray(target) && ("rel" in target || "relAccumulate" in target)) return null;
112
+ if (typeof target === "object" && !Array.isArray(target) && ("relative" in target || "relativeAccumulate" in target)) return null;
108
113
  return resolvePosition(target, nodeIndex);
109
114
  };
110
- /** 浅相等:两个 IRPosition 的两个分量都精确相等(未 round) */
115
+ /** 两个 IRPosition 两分量精确相等(未 round) */
111
116
  var samePoint = (a, b) => !!a && !!b && a[0] === b[0] && a[1] === b[1];
112
117
  /**
113
- * 语义 stroke 档位 → 数值映射(user units)。
114
- *
115
- * 对齐 TikZ 比例(thin = 默认 0.4pt,retikz 默认 strokeWidth = 1,所以 thin → 1):
116
- * ultra thin 0.1pt → 0.25
117
- * very thin 0.2pt → 0.5
118
- * thin 0.4pt → 1 (= 默认 strokeWidth)
119
- * semithick 0.6pt → 1.5
120
- * thick 0.8pt → 2
121
- * very thick 1.2pt → 3
122
- * ultra thick 1.6pt → 4
123
- *
124
- * 显式 `strokeWidth` 始终覆盖 `thickness`——thickness 仅在 strokeWidth 缺省时生效。
118
+ * 语义 stroke 档位 → 数值(user units
119
+ * @description 对齐 TikZ 比例(thin=0.4pt→1=默认 strokeWidth):ultraThin 0.25、veryThin 0.5、thin 1、semithick 1.5、thick 2、veryThick 3、ultraThick 4。显式 strokeWidth 覆盖 thickness
125
120
  */
126
121
  var THICKNESS_TO_WIDTH = {
127
122
  ultraThin: .25,
@@ -132,26 +127,33 @@ var THICKNESS_TO_WIDTH = {
132
127
  veryThick: 3,
133
128
  ultraThick: 4
134
129
  };
135
- /** ADR-0004:边标注的默认字号 / 偏移量(user units) */
130
+ /** 边标注默认字号 / 偏移量 */
136
131
  var LABEL_FONT_SIZE = 14;
137
132
  var LABEL_LINE_HEIGHT_FACTOR = 1.2;
138
133
  var LABEL_SIDE_OFFSET = 4;
139
134
  var RAD_TO_DEG = 180 / Math.PI;
140
- /** label.position段参数 t */
141
- var tForLabelPosition = (pos) => pos === "near-start" ? .25 : pos === "near-end" ? .75 : .5;
135
+ /** keyword → t 数值映射;含旧 3 keyword(midway/near-start/near-end)+ 新 4 keyword */
136
+ var KEYWORD_TO_T = {
137
+ "at-start": 0,
138
+ "very-near-start": .125,
139
+ "near-start": .25,
140
+ midway: .5,
141
+ "near-end": .75,
142
+ "very-near-end": .875,
143
+ "at-end": 1
144
+ };
142
145
  /**
143
- * 把 step.label + 段采样结果翻成 TextPrim(sloped 时再裹一层 group 旋转)。
144
- *
145
- * 几何(默认 side='above',position='midway'):
146
- * - 'above': 锚点 (x, y - LABEL_SIDE_OFFSET),align=middle, baseline=bottom
147
- * - 'below': 锚点 (x, y + LABEL_SIDE_OFFSET),align=middle, baseline=top
148
- * - 'left' : 锚点 (x - LABEL_SIDE_OFFSET, y),align=end, baseline=middle
149
- * - 'right': 锚点 (x + LABEL_SIDE_OFFSET, y),align=start, baseline=middle
150
- * - 'sloped': 锚点 (x, y) 不偏移,align=middle baseline=bottom,外裹 group
151
- * `transform="rotate(angle x y)"`,angle 由切线 atan2 算出(SVG y-down,CW 正向)
152
- *
153
- * 文本宽高交给 measureTextfallbackMeasurer 时只是估算,但不影响渲染坐标。
154
- * 返回 primitive + 用于 viewBox 的若干外接点(sloped 时按"近似最大半径"四角扩张)。
146
+ * label.position 段参数 t∈[0,1]
147
+ * @description 数值原样返回(schema 已 clamp 0..1);keyword 走 KEYWORD_TO_T 映射;undefined 退默认 midway (0.5)
148
+ */
149
+ var tForLabelPosition = (pos) => {
150
+ if (typeof pos === "number") return pos;
151
+ if (typeof pos === "string" && pos in KEYWORD_TO_T) return KEYWORD_TO_T[pos];
152
+ return .5;
153
+ };
154
+ /**
155
+ * step.label + 段采样 → TextPrim(sloped 时裹一层 group 旋转)
156
+ * @description 默认 side='above'/position='midway':above/below 锚点 y±offset、align=middle、baseline=bottom/topleft/right x±offset、align=end/start、baseline=middle;sloped 不偏移裹 group rotate(angle, cx, cy) 由切线 atan2 算(SVG y-down CW 正)。返回 primitive + viewBox 外接点
155
157
  */
156
158
  var emitLabelPrimitive = (label, sample, measureText, round) => {
157
159
  const fontSize = LABEL_FONT_SIZE;
@@ -193,7 +195,12 @@ var emitLabelPrimitive = (label, sample, measureText, round) => {
193
195
  if (side === "sloped") {
194
196
  const groupPrim = {
195
197
  type: "group",
196
- transform: `rotate(${round(Math.atan2(sample.tangent[1], sample.tangent[0]) * RAD_TO_DEG)} ${round(x)} ${round(y)})`,
198
+ transforms: [{
199
+ kind: "rotate",
200
+ degrees: round(Math.atan2(sample.tangent[1], sample.tangent[0]) * RAD_TO_DEG),
201
+ cx: round(x),
202
+ cy: round(y)
203
+ }],
197
204
  children: [text]
198
205
  };
199
206
  const r = Math.max(measuredWidth / 2, measuredHeight / 2);
@@ -220,22 +227,8 @@ var emitLabelPrimitive = (label, sample, measureText, round) => {
220
227
  };
221
228
  };
222
229
  /**
223
- * 把 rel / relAccumulate 目标解析为绝对 Position,产出 step kind 不变但
224
- * `to` 字段全为绝对坐标的步序列。
225
- *
226
- * 决策(ADR-0003 §影响 与 §背景 文本有矛盾):
227
- * 两者都相对 prevEnd 解析,区别仅在是否更新 prevEnd——
228
- * rel 不更新(保持 TikZ `+` 语义),relAccumulate 更新(TikZ `++` 累积)。
229
- * 选这个语义因为:(1) 与 TikZ `+`/`++` 一致;(2) 与字段名"Accumulate"
230
- * 语义匹配;(3) 与 ADR 背景段一致;§影响 段写"pathStart + offset"是 typo。
231
- *
232
- * 跨 step kind 的 prevEnd 推进:
233
- * - 有 to 的 kind(move/line/step/curve/cubic/bend):prevEnd = refPointOfTarget(to)
234
- * - arc:prevEnd = arcEndPoint(prevEnd, radius, endAngle)
235
- * - circlePath / ellipsePath:prevEnd 不变(画完留圆心,即 prevEnd 本身)
236
- * - cycle:prevEnd 不变(不重置到 pathStart,保持简单;后续如有需要再扩)
237
- *
238
- * prevEnd 为 null(首步是 rel)时回退到 [0, 0] 当锚点;解析失败保持原 step。
230
+ * relative/relativeAccumulate 目标解析为绝对 Positionstep kind 不变,to 全为绝对坐标)
231
+ * @description relative 不更新 prevEnd(TikZ `+`),relativeAccumulate 更新(TikZ `++`)。prevEnd 推进:有 to 的 kind 用 refPointOfTarget(to);arc 用 arcEndPoint;circlePath/ellipsePath/cycle 不变。首步 relative 时 prevEnd 回退 [0,0];解析失败保持原 step
239
232
  */
240
233
  var normalizeRelativeTargets = (steps, nodeIndex) => {
241
234
  let prevEnd = null;
@@ -257,13 +250,13 @@ var normalizeRelativeTargets = (steps, nodeIndex) => {
257
250
  const original = step.to;
258
251
  let resolvedTo = original;
259
252
  let updatePrevEnd = true;
260
- if (typeof original === "object" && !Array.isArray(original) && "rel" in original) {
253
+ if (typeof original === "object" && !Array.isArray(original) && "relative" in original) {
261
254
  const ref = prevEnd ?? [0, 0];
262
- resolvedTo = [ref[0] + original.rel[0], ref[1] + original.rel[1]];
255
+ resolvedTo = [ref[0] + original.relative[0], ref[1] + original.relative[1]];
263
256
  updatePrevEnd = false;
264
- } else if (typeof original === "object" && !Array.isArray(original) && "relAccumulate" in original) {
257
+ } else if (typeof original === "object" && !Array.isArray(original) && "relativeAccumulate" in original) {
265
258
  const ref = prevEnd ?? [0, 0];
266
- resolvedTo = [ref[0] + original.relAccumulate[0], ref[1] + original.relAccumulate[1]];
259
+ resolvedTo = [ref[0] + original.relativeAccumulate[0], ref[1] + original.relativeAccumulate[1]];
267
260
  }
268
261
  out.push({
269
262
  ...step,
@@ -277,33 +270,15 @@ var normalizeRelativeTargets = (steps, nodeIndex) => {
277
270
  return out;
278
271
  };
279
272
  /**
280
- * IR Path 翻译为单个 PathPrim
281
- *
282
- * 关键算法(v0.1.0-alpha.1):每个绘制段(line / fold)**独立**地用节点中心
283
- * 算两端 boundary clip——一个节点在路径中段时,"入边"和"出边" boundary 点
284
- * 通常不同,路径会在该节点处可见地"断开"。这与 TikZ 原生语义一致:
285
- *
286
- * `\draw (A) -- (B) -- (C);`
287
- * 段 1:A.center → B.center 决定 A 出口、B 入口的 boundary 交点
288
- * 段 2:B.center → C.center 决定 B 出口、C 入口的 boundary 交点
289
- * B 在两段里 clip 出来的点不同——视觉上看到两条独立线段。
290
- *
291
- * 实现上仍只产一个 PathPrim:d 字符串里以多组 `M ... L ...` 表达多个 sub-path。
292
- * 当某段起点恰好等于上一段终点(例如直接坐标连续,或未触发 clip 差异)时,
293
- * 复用 cursor,省掉冗余 M。
294
- *
295
- * cycle 段:闭回最近一次 move 起点。若 cycle 起点 == lastEnd 且终点 == subPathStart,
296
- * 输出 `Z`(最优雅);否则显式画一段 line(与"段独立 clip"一致)。
297
- *
298
- * 引用未定义节点 / 解析失败时返回 null(path 整体跳过)。
273
+ * IR Path PathPrim
274
+ * @description 每个绘制段独立用节点中心算两端 boundary clip——中段节点的入/出 boundary 点通常不同,path 在该节点可见"断开"(与 TikZ `\draw (A)--(B)--(C);` 段独立 clip 一致)。仍产一个 PathPrim:commands 用多组 move/line 表达 sub-path;段起点等于上段终点时复用 cursor 省 move。cycle 段闭回最近 move 起点,起点==lastEnd && 终点==subPathStart 时输出 close,否则显式画段 line。引用未定义节点/解析失败返回 null
299
275
  */
300
276
  var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer) => {
301
277
  const steps = normalizeRelativeTargets(path.children, nodeIndex);
302
278
  if (steps.length < 2) return null;
303
- /** ADR-0004:每段 step.label 翻译出的 TextPrim(或裹 sloped 旋转的 group),
304
- * 与 path 主体 primitive 同级返回;调用方 push 进 scene.primitives */
279
+ /** 每段 step.label 翻译出的 TextPrim(或 sloped 旋转的 group),与 path 主体同级返回 */
305
280
  const labelPrims = [];
306
- /** 为当前 step 收 label:算 sample 后调用 emitLabelPrimitive,结果累积到 labelPrims / points */
281
+ /** sample emitLabelPrimitive,结果累积到 labelPrims/points */
307
282
  const collectLabel = (step, sampleAt) => {
308
283
  if (step.kind === "move" || step.kind === "cycle" || !("label" in step) || !step.label) return;
309
284
  const sample = sampleAt(tForLabelPosition(step.label.position));
@@ -313,7 +288,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
313
288
  };
314
289
  const hasTo = (s) => s.kind !== "cycle" && s.kind !== "arc" && s.kind !== "circlePath" && s.kind !== "ellipsePath";
315
290
  const anchors = steps.map((s) => hasTo(s) ? refPointOfTarget(s.to, nodeIndex) : null);
316
- /** 找 i 之前最近的"有 to 字段的 step" + 它的 anchor */
291
+ /** 找 i 之前最近的"有 to 字段的 step" 及其 anchor */
317
292
  const findPrev = (i) => {
318
293
  for (let j = i - 1; j >= 0; j--) {
319
294
  const s = steps[j];
@@ -327,7 +302,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
327
302
  }
328
303
  return null;
329
304
  };
330
- /** 找 i 之前最近的 move 的 to——cycle 闭合的目标 */
305
+ /** 找 i 之前最近的 move 的 tocycle 闭合的目标 */
331
306
  const findRecentMoveTo = (i) => {
332
307
  for (let j = i - 1; j >= 0; j--) {
333
308
  const s = steps[j];
@@ -335,80 +310,96 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
335
310
  }
336
311
  return null;
337
312
  };
338
- const ops = [];
313
+ const commands = [];
339
314
  const points = [];
340
315
  let lastEnd = null;
341
316
  let subPathStart = null;
342
317
  /**
343
- * "笔位覆盖"——arc / circlePath / ellipsePath 这种没有 `to` 字段的 step
344
- * 不能通过 `prev.step.to` 重算下一段的起点。它们设置 penOverride,下一个
345
- * 绘制段(line/curve/cubic/bend/step)直接用这个点当 fromClip,之后清空。
346
- *
347
- * - arc:endpoint(弧终点)—— 与 SVG 实际 cursor 一致
348
- * - circlePath/ellipsePath:center(圆心)—— ADR-0002 决策"画完留在圆心",
349
- * 注意 SVG 实际 cursor 在弧端点而不在 center,必须靠 startSegment 发 M
350
- * teleport 回中心
318
+ * 笔位覆盖:arc/circlePath/ellipsePath `to` 字段不能用 prev.step.to 重算起点
319
+ * @description 设置 penOverride 让下个绘制段直接用此点当 fromClip 后清空。arc=弧终点;circlePath/ellipsePath=center("画完留在圆心")
351
320
  */
352
321
  let penOverride = null;
353
- const emitM = (p) => {
354
- ops.push({
355
- cmd: "M",
356
- point: p
322
+ const roundPoint = (p) => [round(p[0]), round(p[1])];
323
+ const emitMove = (p) => {
324
+ const rp = roundPoint(p);
325
+ commands.push({
326
+ kind: "move",
327
+ to: [rp[0], rp[1]]
357
328
  });
358
329
  points.push(p);
359
330
  subPathStart = p;
360
331
  lastEnd = p;
361
332
  };
362
- const emitL = (p) => {
363
- ops.push({
364
- cmd: "L",
365
- point: p
333
+ const emitLine = (p) => {
334
+ const rp = roundPoint(p);
335
+ commands.push({
336
+ kind: "line",
337
+ to: [rp[0], rp[1]]
366
338
  });
367
339
  points.push(p);
368
340
  lastEnd = p;
369
341
  };
370
- const emitZ = () => {
371
- ops.push({ cmd: "Z" });
342
+ const emitClose = () => {
343
+ commands.push({ kind: "close" });
372
344
  lastEnd = subPathStart;
373
345
  };
374
- const emitQ = (control, p) => {
375
- ops.push({
376
- cmd: "Q",
377
- control,
378
- point: p
346
+ const emitQuad = (control, p) => {
347
+ const rc = roundPoint(control);
348
+ const rp = roundPoint(p);
349
+ commands.push({
350
+ kind: "quad",
351
+ control: [rc[0], rc[1]],
352
+ to: [rp[0], rp[1]]
379
353
  });
380
354
  points.push(control);
381
355
  points.push(p);
382
356
  lastEnd = p;
383
357
  };
384
- const emitC = (c1, c2, p) => {
385
- ops.push({
386
- cmd: "C",
387
- control1: c1,
388
- control2: c2,
389
- point: p
358
+ const emitCubic = (c1, c2, p) => {
359
+ const rc1 = roundPoint(c1);
360
+ const rc2 = roundPoint(c2);
361
+ const rp = roundPoint(p);
362
+ commands.push({
363
+ kind: "cubic",
364
+ control1: [rc1[0], rc1[1]],
365
+ control2: [rc2[0], rc2[1]],
366
+ to: [rp[0], rp[1]]
390
367
  });
391
368
  points.push(c1);
392
369
  points.push(c2);
393
370
  points.push(p);
394
371
  lastEnd = p;
395
372
  };
396
- const emitA = (rx, ry, largeArc, sweep, p) => {
397
- ops.push({
398
- cmd: "A",
399
- rx,
400
- ry,
401
- largeArc,
402
- sweep,
403
- point: p
373
+ const emitArc = (center, radius, startAngle, endAngle) => {
374
+ const rc = roundPoint(center);
375
+ commands.push({
376
+ kind: "arc",
377
+ center: [rc[0], rc[1]],
378
+ radius: round(radius),
379
+ startAngle,
380
+ endAngle
404
381
  });
405
- points.push(p);
406
- lastEnd = p;
382
+ points.push(arcEndPoint(center, radius, endAngle));
383
+ lastEnd = arcEndPoint(center, radius, endAngle);
407
384
  };
408
- /** 段起点:与 lastEnd 相同就复用 cursor(省掉冗余 M),否则发 M */
385
+ const emitEllipseArc = (center, radiusX, radiusY, startAngle, endAngle) => {
386
+ const rc = roundPoint(center);
387
+ commands.push({
388
+ kind: "ellipseArc",
389
+ center: [rc[0], rc[1]],
390
+ radiusX: round(radiusX),
391
+ radiusY: round(radiusY),
392
+ startAngle,
393
+ endAngle
394
+ });
395
+ const endPt = [center[0] + Math.cos(endAngle * Math.PI / 180) * radiusX, center[1] + Math.sin(endAngle * Math.PI / 180) * radiusY];
396
+ points.push(endPt);
397
+ lastEnd = endPt;
398
+ };
399
+ /** 段起点:与 lastEnd 相同则复用 cursor(省 move),否则发 move */
409
400
  const startSegment = (p) => {
410
401
  if (samePoint(p, lastEnd)) return;
411
- emitM(p);
402
+ emitMove(p);
412
403
  };
413
404
  for (let i = 0; i < steps.length; i++) {
414
405
  const step = steps[i];
@@ -423,11 +414,11 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
423
414
  const toClip = clipForTarget(moveTo, prev.anchor, nodeIndex);
424
415
  if (!fromClip || !toClip) return null;
425
416
  if (samePoint(fromClip, lastEnd) && samePoint(toClip, subPathStart)) {
426
- emitZ();
417
+ emitClose();
427
418
  continue;
428
419
  }
429
420
  startSegment(fromClip);
430
- emitL(toClip);
421
+ emitLine(toClip);
431
422
  continue;
432
423
  }
433
424
  const prev = findPrev(i);
@@ -436,9 +427,8 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
436
427
  const center = prev.anchor;
437
428
  const startPt = arcEndPoint(center, step.radius, step.startAngle);
438
429
  const endPt = arcEndPoint(center, step.radius, step.endAngle);
439
- const flags = arcSvgFlags(step.startAngle, step.endAngle);
440
430
  startSegment(startPt);
441
- emitA(step.radius, step.radius, flags.largeArc, flags.sweep, endPt);
431
+ emitArc(center, step.radius, step.startAngle, step.endAngle);
442
432
  for (const p of arcBoundingPoints(center, step.radius, step.startAngle, step.endAngle)) points.push(p);
443
433
  collectLabel(step, (t) => arcSegmentSample(center, step.radius, step.startAngle, step.endAngle, t));
444
434
  penOverride = endPt;
@@ -447,11 +437,8 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
447
437
  if (step.kind === "circlePath") {
448
438
  const center = prev.anchor;
449
439
  const r = step.radius;
450
- const right = [center[0] + r, center[1]];
451
- const left = [center[0] - r, center[1]];
452
- startSegment(right);
453
- emitA(r, r, 0, 1, left);
454
- emitA(r, r, 0, 1, right);
440
+ startSegment([center[0] + r, center[1]]);
441
+ emitEllipseArc(center, r, r, 0, 360);
455
442
  points.push([center[0] + r, center[1]]);
456
443
  points.push([center[0] - r, center[1]]);
457
444
  points.push([center[0], center[1] + r]);
@@ -464,11 +451,8 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
464
451
  const center = prev.anchor;
465
452
  const rx = step.radiusX;
466
453
  const ry = step.radiusY;
467
- const right = [center[0] + rx, center[1]];
468
- const left = [center[0] - rx, center[1]];
469
- startSegment(right);
470
- emitA(rx, ry, 0, 1, left);
471
- emitA(rx, ry, 0, 1, right);
454
+ startSegment([center[0] + rx, center[1]]);
455
+ emitEllipseArc(center, rx, ry, 0, 360);
472
456
  points.push([center[0] + rx, center[1]]);
473
457
  points.push([center[0] - rx, center[1]]);
474
458
  points.push([center[0], center[1] + ry]);
@@ -486,7 +470,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
486
470
  const toClip = clipForTarget(step.to, prev.anchor, nodeIndex);
487
471
  if (!fromClip || !toClip) return null;
488
472
  startSegment(fromClip);
489
- emitL(toClip);
473
+ emitLine(toClip);
490
474
  collectLabel(step, (t) => lineSegmentSample(fromClip, toClip, t));
491
475
  continue;
492
476
  }
@@ -495,7 +479,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
495
479
  const toClip = clipForTarget(step.to, step.control, nodeIndex);
496
480
  if (!fromClip || !toClip) return null;
497
481
  startSegment(fromClip);
498
- emitQ(step.control, toClip);
482
+ emitQuad(step.control, toClip);
499
483
  collectLabel(step, (t) => quadSegmentSample(fromClip, step.control, toClip, t));
500
484
  continue;
501
485
  }
@@ -504,7 +488,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
504
488
  const toClip = clipForTarget(step.to, step.control2, nodeIndex);
505
489
  if (!fromClip || !toClip) return null;
506
490
  startSegment(fromClip);
507
- emitC(step.control1, step.control2, toClip);
491
+ emitCubic(step.control1, step.control2, toClip);
508
492
  collectLabel(step, (t) => cubicSegmentSample(fromClip, step.control1, step.control2, toClip, t));
509
493
  continue;
510
494
  }
@@ -515,7 +499,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
515
499
  const toClip = clipForTarget(step.to, c2, nodeIndex);
516
500
  if (!fromClip || !toClip) return null;
517
501
  startSegment(fromClip);
518
- emitC(c1, c2, toClip);
502
+ emitCubic(c1, c2, toClip);
519
503
  collectLabel(step, (t) => cubicSegmentSample(fromClip, c1, c2, toClip, t));
520
504
  continue;
521
505
  }
@@ -524,8 +508,8 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
524
508
  const toClip = clipForTarget(step.to, corner, nodeIndex);
525
509
  if (!fromClip || !toClip) return null;
526
510
  startSegment(fromClip);
527
- emitL(corner);
528
- emitL(toClip);
511
+ emitLine(corner);
512
+ emitLine(toClip);
529
513
  collectLabel(step, (t) => foldSegmentSample(fromClip, corner, toClip, t));
530
514
  }
531
515
  const strokeWidth = path.strokeWidth ?? (path.thickness ? THICKNESS_TO_WIDTH[path.thickness] : 1);
@@ -541,49 +525,84 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
541
525
  fillOpacity: path.fillOpacity,
542
526
  strokeOpacity: path.drawOpacity
543
527
  };
544
- const markers = arrowMarkers(path.arrow, path.arrowShape);
528
+ const markers = arrowMarkers(path.arrow, path.arrowDetail);
545
529
  const hasArrows = !!markers.arrowStart || !!markers.arrowEnd;
546
- const shrinkStart = markers.arrowStart ? SHRINK_FOR_SHAPE[markers.arrowStart] : 0;
547
- const shrinkEnd = markers.arrowEnd ? SHRINK_FOR_SHAPE[markers.arrowEnd] : 0;
530
+ const shrinkStart = markers.arrowStart ? computeShrink(markers.arrowStart) : 0;
531
+ const shrinkEnd = markers.arrowEnd ? computeShrink(markers.arrowEnd) : 0;
532
+ /** 取一个 PathCommand 末端 endpoint(move/line/quad/cubic → to;arc/ellipseArc → polar(end);close 无端点) */
533
+ const endpointOf = (cmd) => {
534
+ switch (cmd.kind) {
535
+ case "move":
536
+ case "line":
537
+ case "quad":
538
+ case "cubic": return [cmd.to[0], cmd.to[1]];
539
+ case "arc": {
540
+ const rad = cmd.endAngle * Math.PI / 180;
541
+ return [cmd.center[0] + Math.cos(rad) * cmd.radius, cmd.center[1] + Math.sin(rad) * cmd.radius];
542
+ }
543
+ case "ellipseArc": {
544
+ const rad = cmd.endAngle * Math.PI / 180;
545
+ return [cmd.center[0] + Math.cos(rad) * cmd.radiusX, cmd.center[1] + Math.sin(rad) * cmd.radiusY];
546
+ }
547
+ case "close": return null;
548
+ }
549
+ };
550
+ /** 改写一个 PathCommand 的 endpoint(用于 shrink) */
551
+ const setEndpoint = (idx, newPt) => {
552
+ const cmd = commands[idx];
553
+ if (cmd.kind === "close") return;
554
+ const rp = [round(newPt[0]), round(newPt[1])];
555
+ if (cmd.kind === "move" || cmd.kind === "line") commands[idx] = {
556
+ ...cmd,
557
+ to: rp
558
+ };
559
+ else if (cmd.kind === "quad") commands[idx] = {
560
+ ...cmd,
561
+ to: rp
562
+ };
563
+ else if (cmd.kind === "cubic") commands[idx] = {
564
+ ...cmd,
565
+ to: rp
566
+ };
567
+ };
548
568
  if (shrinkStart > 0) {
549
- const firstIdx = ops.findIndex((o) => o.cmd === "M");
569
+ const firstIdx = commands.findIndex((o) => o.kind === "move");
550
570
  if (firstIdx >= 0) {
551
- const cur = ops[firstIdx];
552
- const next = ops.slice(firstIdx + 1).find((o) => o.cmd !== "Z");
553
- if (cur.cmd !== "Z" && next) cur.point = shiftToward(cur.point, next.point, shrinkStart * strokeWidth);
571
+ const cur = commands[firstIdx];
572
+ const nextIdx = commands.findIndex((o, idx) => idx > firstIdx && o.kind !== "close");
573
+ if (cur.kind === "move" && nextIdx >= 0) {
574
+ const nextPt = endpointOf(commands[nextIdx]);
575
+ if (nextPt) setEndpoint(firstIdx, shiftToward([cur.to[0], cur.to[1]], nextPt, shrinkStart * strokeWidth));
576
+ }
554
577
  }
555
578
  }
556
579
  if (shrinkEnd > 0) {
557
580
  let lastIdx = -1;
558
- for (let i = ops.length - 1; i >= 0; i--) if (ops[i].cmd !== "Z") {
581
+ for (let i = commands.length - 1; i >= 0; i--) if (commands[i].kind !== "close") {
559
582
  lastIdx = i;
560
583
  break;
561
584
  }
562
585
  if (lastIdx > 0) {
563
586
  let prevIdx = lastIdx - 1;
564
- while (prevIdx >= 0 && ops[prevIdx].cmd === "Z") prevIdx--;
587
+ while (prevIdx >= 0 && commands[prevIdx].kind === "close") prevIdx--;
565
588
  if (prevIdx >= 0) {
566
- const cur = ops[lastIdx];
567
- const prev = ops[prevIdx];
568
- if (cur.cmd !== "Z" && prev.cmd !== "Z") cur.point = shiftToward(cur.point, prev.point, shrinkEnd * strokeWidth);
589
+ const curPt = endpointOf(commands[lastIdx]);
590
+ const prevPt = endpointOf(commands[prevIdx]);
591
+ if (curPt && prevPt) {
592
+ const shifted = shiftToward(curPt, prevPt, shrinkEnd * strokeWidth);
593
+ setEndpoint(lastIdx, shifted);
594
+ }
569
595
  }
570
596
  }
571
597
  }
572
- const tokens = ops.map((op) => {
573
- if (op.cmd === "Z") return "Z";
574
- if (op.cmd === "Q") return `Q ${round(op.control[0])} ${round(op.control[1])} ${round(op.point[0])} ${round(op.point[1])}`;
575
- if (op.cmd === "C") return `C ${round(op.control1[0])} ${round(op.control1[1])} ${round(op.control2[0])} ${round(op.control2[1])} ${round(op.point[0])} ${round(op.point[1])}`;
576
- if (op.cmd === "A") return `A ${round(op.rx)} ${round(op.ry)} 0 ${op.largeArc} ${op.sweep} ${round(op.point[0])} ${round(op.point[1])}`;
577
- return `${op.cmd} ${round(op.point[0])} ${round(op.point[1])}`;
578
- });
579
598
  const subPathStarts = [];
580
- tokens.forEach((tok, idx) => {
581
- if (tok.startsWith("M ")) subPathStarts.push(idx);
599
+ commands.forEach((cmd, idx) => {
600
+ if (cmd.kind === "move") subPathStarts.push(idx);
582
601
  });
583
602
  if (!hasArrows || subPathStarts.length <= 1) return {
584
603
  primitives: [{
585
604
  type: "path",
586
- d: tokens.join(" "),
605
+ commands,
587
606
  ...baseProps,
588
607
  ...markers
589
608
  }, ...labelPrims],
@@ -592,8 +611,8 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
592
611
  const subPathSlices = [];
593
612
  for (let s = 0; s < subPathStarts.length; s++) {
594
613
  const start = subPathStarts[s];
595
- const end = s + 1 < subPathStarts.length ? subPathStarts[s + 1] : tokens.length;
596
- subPathSlices.push(tokens.slice(start, end));
614
+ const end = s + 1 < subPathStarts.length ? subPathStarts[s + 1] : commands.length;
615
+ subPathSlices.push(commands.slice(start, end));
597
616
  }
598
617
  return {
599
618
  primitives: [{
@@ -603,7 +622,7 @@ var emitPathPrimitive = (path, nodeIndex, round, measureText = fallbackMeasurer)
603
622
  const isLast = i === subPathSlices.length - 1;
604
623
  return {
605
624
  type: "path",
606
- d: sub.join(" "),
625
+ commands: sub,
607
626
  ...baseProps,
608
627
  ...isFirst && markers.arrowStart ? { arrowStart: markers.arrowStart } : {},
609
628
  ...isLast && markers.arrowEnd ? { arrowEnd: markers.arrowEnd } : {}