@faicad/faijs 0.18.3 → 0.22.2

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 (241) hide show
  1. package/dist/api/api-namespace.d.ts.map +1 -1
  2. package/dist/api/api-namespace.js +4 -1
  3. package/dist/api/api-namespace.js.map +1 -1
  4. package/dist/api/brep-mirror/joinery-brep.d.ts +1 -1
  5. package/dist/api/brep-mirror/joinery-brep.d.ts.map +1 -1
  6. package/dist/api/brep-mirror/joinery-brep.js +22 -20
  7. package/dist/api/brep-mirror/joinery-brep.js.map +1 -1
  8. package/dist/api/generated/script-face-manifest.d.ts +4 -0
  9. package/dist/api/generated/script-face-manifest.d.ts.map +1 -1
  10. package/dist/api/generated/script-face-manifest.js.map +1 -1
  11. package/dist/api/index.d.ts +4 -0
  12. package/dist/api/index.d.ts.map +1 -1
  13. package/dist/api/index.js +3 -0
  14. package/dist/api/index.js.map +1 -1
  15. package/dist/api/knurl.d.ts.map +1 -1
  16. package/dist/api/knurl.js +1 -0
  17. package/dist/api/knurl.js.map +1 -1
  18. package/dist/api/primitives.d.ts +4 -3
  19. package/dist/api/primitives.d.ts.map +1 -1
  20. package/dist/api/primitives.js +9 -3
  21. package/dist/api/primitives.js.map +1 -1
  22. package/dist/api/profile.d.ts +34 -5
  23. package/dist/api/profile.d.ts.map +1 -1
  24. package/dist/api/profile.js +168 -186
  25. package/dist/api/profile.js.map +1 -1
  26. package/dist/api/punch-hole.d.ts +77 -0
  27. package/dist/api/punch-hole.d.ts.map +1 -0
  28. package/dist/api/punch-hole.js +207 -0
  29. package/dist/api/punch-hole.js.map +1 -0
  30. package/dist/api/sketch-on-face.d.ts +85 -0
  31. package/dist/api/sketch-on-face.d.ts.map +1 -0
  32. package/dist/api/sketch-on-face.js +136 -0
  33. package/dist/api/sketch-on-face.js.map +1 -0
  34. package/dist/api/sketch-on-plane.d.ts +87 -0
  35. package/dist/api/sketch-on-plane.d.ts.map +1 -0
  36. package/dist/api/sketch-on-plane.js +106 -0
  37. package/dist/api/sketch-on-plane.js.map +1 -0
  38. package/dist/api/surface/arg-spec.d.ts +8 -0
  39. package/dist/api/surface/arg-spec.d.ts.map +1 -1
  40. package/dist/api/surface/arg-spec.js.map +1 -1
  41. package/dist/api/transform.d.ts +5 -5
  42. package/dist/api/transform.d.ts.map +1 -1
  43. package/dist/api/transform.js +12 -10
  44. package/dist/api/transform.js.map +1 -1
  45. package/dist/api/xml-dom.d.ts +27 -0
  46. package/dist/api/xml-dom.d.ts.map +1 -0
  47. package/dist/api/xml-dom.js +45 -0
  48. package/dist/api/xml-dom.js.map +1 -0
  49. package/dist/boolean/csg-backend.d.ts +6 -6
  50. package/dist/boolean/csg-backend.d.ts.map +1 -1
  51. package/dist/boolean/csg-backend.js +9 -9
  52. package/dist/boolean/csg-backend.js.map +1 -1
  53. package/dist/boolean/csg-core.d.ts +10 -10
  54. package/dist/boolean/csg-core.d.ts.map +1 -1
  55. package/dist/boolean/csg-core.js +24 -24
  56. package/dist/boolean/csg-core.js.map +1 -1
  57. package/dist/boolean/extrude-helpers.d.ts +8 -8
  58. package/dist/boolean/extrude-helpers.d.ts.map +1 -1
  59. package/dist/boolean/extrude-helpers.js +13 -13
  60. package/dist/boolean/extrude-helpers.js.map +1 -1
  61. package/dist/brep/brep-ops.d.ts +24 -10
  62. package/dist/brep/brep-ops.d.ts.map +1 -1
  63. package/dist/brep/brep-ops.js +46 -18
  64. package/dist/brep/brep-ops.js.map +1 -1
  65. package/dist/brep/brep-topology.d.ts.map +1 -1
  66. package/dist/brep/brep-topology.js +3 -1
  67. package/dist/brep/brep-topology.js.map +1 -1
  68. package/dist/brep/export/export-model.d.ts +79 -0
  69. package/dist/brep/export/export-model.d.ts.map +1 -0
  70. package/dist/brep/export/export-model.js +335 -0
  71. package/dist/brep/export/export-model.js.map +1 -0
  72. package/dist/brep/primitives-brep.d.ts.map +1 -1
  73. package/dist/brep/primitives-brep.js +3 -1
  74. package/dist/brep/primitives-brep.js.map +1 -1
  75. package/dist/brepkit-kernel/brepkitKernel.d.ts.map +1 -1
  76. package/dist/brepkit-kernel/brepkitKernel.js +4 -2
  77. package/dist/brepkit-kernel/brepkitKernel.js.map +1 -1
  78. package/dist/browser-host/csg-worker-protocol.d.ts +3 -3
  79. package/dist/browser-host/csg-worker-protocol.d.ts.map +1 -1
  80. package/dist/browser-host/csg-worker-protocol.js.map +1 -1
  81. package/dist/browser-host/csg-worker.js +3 -3
  82. package/dist/browser-host/csg-worker.js.map +1 -1
  83. package/dist/browser-host/inline-csg-backend.d.ts +3 -3
  84. package/dist/browser-host/inline-csg-backend.d.ts.map +1 -1
  85. package/dist/browser-host/inline-csg-backend.js +3 -3
  86. package/dist/browser-host/inline-csg-backend.js.map +1 -1
  87. package/dist/browser-host/worker-csg-backend.d.ts +3 -3
  88. package/dist/browser-host/worker-csg-backend.d.ts.map +1 -1
  89. package/dist/browser-host/worker-csg-backend.js.map +1 -1
  90. package/dist/cad-runtime/direct-executor.d.ts +4 -0
  91. package/dist/cad-runtime/direct-executor.d.ts.map +1 -1
  92. package/dist/cad-runtime/direct-executor.js +17 -1
  93. package/dist/cad-runtime/direct-executor.js.map +1 -1
  94. package/dist/cad-runtime/ports.d.ts +3 -3
  95. package/dist/cad-runtime/ports.d.ts.map +1 -1
  96. package/dist/cad-runtime/ports.js.map +1 -1
  97. package/dist/cad-runtime/runtime.d.ts +24 -1
  98. package/dist/cad-runtime/runtime.d.ts.map +1 -1
  99. package/dist/cad-runtime/runtime.js +53 -3
  100. package/dist/cad-runtime/runtime.js.map +1 -1
  101. package/dist/define-op.d.ts +22 -0
  102. package/dist/define-op.d.ts.map +1 -1
  103. package/dist/define-op.js +15 -0
  104. package/dist/define-op.js.map +1 -1
  105. package/dist/env-agnostic.d.ts +7 -7
  106. package/dist/env-agnostic.d.ts.map +1 -1
  107. package/dist/env-agnostic.js +3 -3
  108. package/dist/env-agnostic.js.map +1 -1
  109. package/dist/geometry2d/adapt.d.ts +46 -0
  110. package/dist/geometry2d/adapt.d.ts.map +1 -0
  111. package/dist/geometry2d/adapt.js +41 -0
  112. package/dist/geometry2d/adapt.js.map +1 -0
  113. package/dist/geometry2d/bbox2d.d.ts +82 -0
  114. package/dist/geometry2d/bbox2d.d.ts.map +1 -0
  115. package/dist/geometry2d/bbox2d.js +123 -0
  116. package/dist/geometry2d/bbox2d.js.map +1 -0
  117. package/dist/geometry2d/blueprint-sketcher.d.ts +27 -0
  118. package/dist/geometry2d/blueprint-sketcher.d.ts.map +1 -0
  119. package/dist/geometry2d/blueprint-sketcher.js +29 -0
  120. package/dist/geometry2d/blueprint-sketcher.js.map +1 -0
  121. package/dist/geometry2d/blueprint.d.ts +102 -0
  122. package/dist/geometry2d/blueprint.d.ts.map +1 -0
  123. package/dist/geometry2d/blueprint.js +225 -0
  124. package/dist/geometry2d/blueprint.js.map +1 -0
  125. package/dist/geometry2d/blueprints.d.ts +58 -0
  126. package/dist/geometry2d/blueprints.d.ts.map +1 -0
  127. package/dist/geometry2d/blueprints.js +72 -0
  128. package/dist/geometry2d/blueprints.js.map +1 -0
  129. package/dist/geometry2d/bridge/lift-on-plane.d.ts +50 -0
  130. package/dist/geometry2d/bridge/lift-on-plane.d.ts.map +1 -0
  131. package/dist/geometry2d/bridge/lift-on-plane.js +78 -0
  132. package/dist/geometry2d/bridge/lift-on-plane.js.map +1 -0
  133. package/dist/geometry2d/bridge/on-surface.d.ts +98 -0
  134. package/dist/geometry2d/bridge/on-surface.d.ts.map +1 -0
  135. package/dist/geometry2d/bridge/on-surface.js +94 -0
  136. package/dist/geometry2d/bridge/on-surface.js.map +1 -0
  137. package/dist/geometry2d/bridge/plane.d.ts +69 -0
  138. package/dist/geometry2d/bridge/plane.d.ts.map +1 -0
  139. package/dist/geometry2d/bridge/plane.js +96 -0
  140. package/dist/geometry2d/bridge/plane.js.map +1 -0
  141. package/dist/geometry2d/canned-blueprints.d.ts +44 -0
  142. package/dist/geometry2d/canned-blueprints.d.ts.map +1 -0
  143. package/dist/geometry2d/canned-blueprints.js +94 -0
  144. package/dist/geometry2d/canned-blueprints.js.map +1 -0
  145. package/dist/geometry2d/compound-blueprint.d.ts +72 -0
  146. package/dist/geometry2d/compound-blueprint.d.ts.map +1 -0
  147. package/dist/geometry2d/compound-blueprint.js +95 -0
  148. package/dist/geometry2d/compound-blueprint.js.map +1 -0
  149. package/dist/geometry2d/curve2d.d.ts +298 -0
  150. package/dist/geometry2d/curve2d.d.ts.map +1 -0
  151. package/dist/geometry2d/curve2d.js +1110 -0
  152. package/dist/geometry2d/curve2d.js.map +1 -0
  153. package/dist/geometry2d/index.d.ts +22 -0
  154. package/dist/geometry2d/index.d.ts.map +1 -0
  155. package/dist/geometry2d/index.js +21 -0
  156. package/dist/geometry2d/index.js.map +1 -0
  157. package/dist/geometry2d/organise.d.ts +28 -0
  158. package/dist/geometry2d/organise.d.ts.map +1 -0
  159. package/dist/geometry2d/organise.js +108 -0
  160. package/dist/geometry2d/organise.js.map +1 -0
  161. package/dist/geometry2d/pen-sketcher.d.ts +365 -0
  162. package/dist/geometry2d/pen-sketcher.d.ts.map +1 -0
  163. package/dist/geometry2d/pen-sketcher.js +520 -0
  164. package/dist/geometry2d/pen-sketcher.js.map +1 -0
  165. package/dist/geometry2d/point.d.ts +44 -0
  166. package/dist/geometry2d/point.d.ts.map +1 -0
  167. package/dist/geometry2d/point.js +57 -0
  168. package/dist/geometry2d/point.js.map +1 -0
  169. package/dist/geometry2d/svg-ellipse.d.ts +74 -0
  170. package/dist/geometry2d/svg-ellipse.d.ts.map +1 -0
  171. package/dist/geometry2d/svg-ellipse.js +163 -0
  172. package/dist/geometry2d/svg-ellipse.js.map +1 -0
  173. package/dist/lang/code-to-args.d.ts +29 -3
  174. package/dist/lang/code-to-args.d.ts.map +1 -1
  175. package/dist/lang/code-to-args.js +41 -2
  176. package/dist/lang/code-to-args.js.map +1 -1
  177. package/dist/lang/codegen.d.ts +45 -1
  178. package/dist/lang/codegen.d.ts.map +1 -1
  179. package/dist/lang/codegen.js +135 -6
  180. package/dist/lang/codegen.js.map +1 -1
  181. package/dist/lang/host-arg.d.ts +6 -0
  182. package/dist/lang/host-arg.d.ts.map +1 -1
  183. package/dist/lang/host-arg.js.map +1 -1
  184. package/dist/lang/metadata-extractor.d.ts +25 -0
  185. package/dist/lang/metadata-extractor.d.ts.map +1 -1
  186. package/dist/lang/metadata-extractor.js +289 -25
  187. package/dist/lang/metadata-extractor.js.map +1 -1
  188. package/dist/lang/parse-error.d.ts +7 -1
  189. package/dist/lang/parse-error.d.ts.map +1 -1
  190. package/dist/lang/parse-error.js.map +1 -1
  191. package/dist/lang/security-scanner.d.ts +9 -1
  192. package/dist/lang/security-scanner.d.ts.map +1 -1
  193. package/dist/lang/security-scanner.js +38 -1
  194. package/dist/lang/security-scanner.js.map +1 -1
  195. package/dist/lang/symbol-table.generated.d.ts +3 -0
  196. package/dist/lang/symbol-table.generated.d.ts.map +1 -1
  197. package/dist/lang/symbol-table.generated.js +3 -0
  198. package/dist/lang/symbol-table.generated.js.map +1 -1
  199. package/dist/mesh/io.d.ts +62 -9
  200. package/dist/mesh/io.d.ts.map +1 -1
  201. package/dist/mesh/io.js +124 -17
  202. package/dist/mesh/io.js.map +1 -1
  203. package/dist/mesh/threemf-loader.d.ts +15 -0
  204. package/dist/mesh/threemf-loader.d.ts.map +1 -0
  205. package/dist/mesh/threemf-loader.js +163 -0
  206. package/dist/mesh/threemf-loader.js.map +1 -0
  207. package/dist/mesh/transform.d.ts +2 -2
  208. package/dist/mesh/transform.d.ts.map +1 -1
  209. package/dist/mesh/transform.js +3 -3
  210. package/dist/mesh/transform.js.map +1 -1
  211. package/dist/mesh/types.d.ts +2 -2
  212. package/dist/mesh/types.d.ts.map +1 -1
  213. package/dist/mesh/types.js.map +1 -1
  214. package/dist/node-host/cli.d.ts.map +1 -1
  215. package/dist/node-host/cli.js +53 -26
  216. package/dist/node-host/cli.js.map +1 -1
  217. package/dist/occt-kernel/occtKernel.d.ts.map +1 -1
  218. package/dist/occt-kernel/occtKernel.js +4 -2
  219. package/dist/occt-kernel/occtKernel.js.map +1 -1
  220. package/dist/occt-kernel/topologyExt.d.ts.map +1 -1
  221. package/dist/occt-kernel/topologyExt.js +4 -1
  222. package/dist/occt-kernel/topologyExt.js.map +1 -1
  223. package/dist/runtime-state.d.ts +18 -0
  224. package/dist/runtime-state.d.ts.map +1 -1
  225. package/dist/runtime-state.js +23 -0
  226. package/dist/runtime-state.js.map +1 -1
  227. package/dist/sdk.d.ts +1 -1
  228. package/dist/sdk.d.ts.map +1 -1
  229. package/dist/sdk.js +1 -1
  230. package/dist/sdk.js.map +1 -1
  231. package/dist/tolerance.d.ts +38 -0
  232. package/dist/tolerance.d.ts.map +1 -0
  233. package/dist/tolerance.js +40 -0
  234. package/dist/tolerance.js.map +1 -0
  235. package/dist/topology/naming/score.js +3 -3
  236. package/dist/topology/naming/score.js.map +1 -1
  237. package/dist/units.d.ts +248 -0
  238. package/dist/units.d.ts.map +1 -0
  239. package/dist/units.js +397 -0
  240. package/dist/units.js.map +1 -0
  241. package/package.json +19 -2
@@ -0,0 +1,1110 @@
1
+ /**
2
+ * Pure-TypeScript 2D analytic curve model (faijs port of brepjs).
3
+ *
4
+ * Adapted from brepjs `src/kernel/geometry2d.ts` (Apache-2.0), preserving core
5
+ * algorithms and naming. Differences from brepjs:
6
+ * - discriminant field renamed `__bk2d` → `kind2d` (2026-09-27 decision),
7
+ * bbox discriminant `__bk2d_bbox` → `kind2d_b`.
8
+ * - All curves are plain data objects (no kernel handles, no dispose), per the
9
+ * faijs "2D curves are pure objects" design (docs/plans/2d-sketching: A1/B1).
10
+ *
11
+ * Pure TS, zero WASM, zero external deps. All 2D curves represented as plain
12
+ * objects with a `kind2d` discriminant.
13
+ *
14
+ * License: adapted from brepjs (Apache-2.0). Retained per faijs convention for
15
+ * brepjs-ported files (cf. svg-to-solid.ts).
16
+ *
17
+ * @module
18
+ */
19
+ // ---------------------------------------------------------------------------
20
+ // Evaluation
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Evaluate a curve at parameter t to a 2D point.
24
+ * @param c - the curve to evaluate.
25
+ * @param t - the parameter value within the curve's domain.
26
+ * @returns the 2D point `[x, y]` on the curve.
27
+ */
28
+ export function evaluateCurve2d(c, t) {
29
+ switch (c.kind2d) {
30
+ case 'line':
31
+ return [c.ox + c.dx * t, c.oy + c.dy * t];
32
+ case 'circle': {
33
+ const angle = c.sense ? t : -t;
34
+ return [c.cx + c.radius * Math.cos(angle), c.cy + c.radius * Math.sin(angle)];
35
+ }
36
+ case 'ellipse': {
37
+ const angle = c.sense ? t : -t;
38
+ const cos = Math.cos(c.xDirAngle);
39
+ const sin = Math.sin(c.xDirAngle);
40
+ const x = c.majorRadius * Math.cos(angle);
41
+ const y = c.minorRadius * Math.sin(angle);
42
+ return [c.cx + x * cos - y * sin, c.cy + x * sin + y * cos];
43
+ }
44
+ case 'bezier':
45
+ return evaluateBezier(c.poles, t);
46
+ case 'bspline':
47
+ return evaluateBSpline2d(c, t);
48
+ case 'trimmed': {
49
+ const mapped = c.tStart + t * (c.tEnd - c.tStart);
50
+ return evaluateCurve2d(c.basis, mapped);
51
+ }
52
+ }
53
+ }
54
+ /**
55
+ * Tangent (non-normalized direction derivative) at parameter t.
56
+ * @param c - the curve to differentiate.
57
+ * @param t - the parameter value within the curve's domain.
58
+ * @returns the unnormalized tangent vector `[dx, dy]` at `t`.
59
+ */
60
+ export function tangentCurve2d(c, t) {
61
+ switch (c.kind2d) {
62
+ case 'line':
63
+ return [c.dx, c.dy];
64
+ case 'circle': {
65
+ const angle = c.sense ? t : -t;
66
+ const sign = c.sense ? 1 : -1;
67
+ return [-c.radius * Math.sin(angle) * sign, c.radius * Math.cos(angle) * sign];
68
+ }
69
+ case 'ellipse': {
70
+ const angle = c.sense ? t : -t;
71
+ const sign = c.sense ? 1 : -1;
72
+ const cos = Math.cos(c.xDirAngle);
73
+ const sin = Math.sin(c.xDirAngle);
74
+ const dx = -c.majorRadius * Math.sin(angle) * sign;
75
+ const dy = c.minorRadius * Math.cos(angle) * sign;
76
+ return [dx * cos - dy * sin, dx * sin + dy * cos];
77
+ }
78
+ case 'bezier': {
79
+ // Numerical differentiation
80
+ const h = 1e-8;
81
+ const p0 = evaluateBezier(c.poles, Math.max(0, t - h));
82
+ const p1 = evaluateBezier(c.poles, Math.min(1, t + h));
83
+ const dt = Math.min(1, t + h) - Math.max(0, t - h);
84
+ return [(p1[0] - p0[0]) / dt, (p1[1] - p0[1]) / dt];
85
+ }
86
+ case 'bspline': {
87
+ const h = 1e-8;
88
+ const kFirst = c.knots[0];
89
+ const kLast = c.knots[c.knots.length - 1];
90
+ const p0 = evaluateBSpline2d(c, Math.max(kFirst, t - h));
91
+ const p1 = evaluateBSpline2d(c, Math.min(kLast, t + h));
92
+ const dt = Math.min(kLast, t + h) - Math.max(kFirst, t - h);
93
+ return [(p1[0] - p0[0]) / dt, (p1[1] - p0[1]) / dt];
94
+ }
95
+ case 'trimmed': {
96
+ const mapped = c.tStart + t * (c.tEnd - c.tStart);
97
+ const tan = tangentCurve2d(c.basis, mapped);
98
+ const scale = c.tEnd - c.tStart;
99
+ return [tan[0] * scale, tan[1] * scale];
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * Parameter domain of a curve.
105
+ * @param c - the curve whose domain to read.
106
+ * @returns the inclusive `first` and `last` parameter bounds.
107
+ */
108
+ export function curveBounds(c) {
109
+ switch (c.kind2d) {
110
+ case 'line':
111
+ return { first: 0, last: c.len };
112
+ case 'circle':
113
+ case 'ellipse':
114
+ return { first: 0, last: 2 * Math.PI };
115
+ case 'bezier':
116
+ return { first: 0, last: 1 };
117
+ case 'bspline':
118
+ return { first: c.knots[0], last: c.knots[c.knots.length - 1] };
119
+ case 'trimmed':
120
+ return { first: 0, last: 1 };
121
+ }
122
+ }
123
+ /**
124
+ * OCCT-style type name of a curve.
125
+ * @param c - the curve to name.
126
+ * @returns the uppercase OCCT type name such as `LINE`, `CIRCLE` or `BSPLINE_CURVE`.
127
+ */
128
+ export function curveTypeName(c) {
129
+ switch (c.kind2d) {
130
+ case 'line':
131
+ return 'LINE';
132
+ case 'circle':
133
+ return 'CIRCLE';
134
+ case 'ellipse':
135
+ return 'ELLIPSE';
136
+ case 'bezier':
137
+ return 'BEZIER_CURVE';
138
+ case 'bspline':
139
+ return 'BSPLINE_CURVE';
140
+ case 'trimmed':
141
+ return 'TRIMMED_' + curveTypeName(c.basis);
142
+ }
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // Construction helpers
146
+ // ---------------------------------------------------------------------------
147
+ /**
148
+ * Line segment from (x1,y1) to (x2,y2).
149
+ * @param x1 - the x coordinate of the start point.
150
+ * @param y1 - the y coordinate of the start point.
151
+ * @param x2 - the x coordinate of the end point.
152
+ * @param y2 - the y coordinate of the end point.
153
+ * @returns a Line2d describing the segment.
154
+ */
155
+ export function makeLine2d(x1, y1, x2, y2) {
156
+ const dx = x2 - x1;
157
+ const dy = y2 - y1;
158
+ const len = Math.sqrt(dx * dx + dy * dy);
159
+ return {
160
+ kind2d: 'line',
161
+ ox: x1,
162
+ oy: y1,
163
+ dx: len > 0 ? dx / len : 1,
164
+ dy: len > 0 ? dy / len : 0,
165
+ len,
166
+ };
167
+ }
168
+ /**
169
+ * Full circle with the given center, radius and winding sense.
170
+ * @param cx - the x coordinate of the center.
171
+ * @param cy - the y coordinate of the center.
172
+ * @param radius - the circle radius.
173
+ * @param sense - true for counter-clockwise winding (default true).
174
+ * @returns a Circle2d.
175
+ */
176
+ export function makeCircle2d(cx, cy, radius, sense = true) {
177
+ return { kind2d: 'circle', cx, cy, radius, sense };
178
+ }
179
+ /**
180
+ * Arc through three points; returns a trimmed circle, or a line if collinear.
181
+ * @param x1 - the x coordinate of the start point.
182
+ * @param y1 - the y coordinate of the start point.
183
+ * @param xm - the x coordinate of the mid point.
184
+ * @param ym - the y coordinate of the mid point.
185
+ * @param x2 - the x coordinate of the end point.
186
+ * @param y2 - the y coordinate of the end point.
187
+ * @returns a Curve2dObj: a trimmed arc circle, or a straight line when the points are collinear.
188
+ */
189
+ export function makeArc2dThreePoints(x1, y1, xm, ym, x2, y2) {
190
+ // Circumscribed circle through 3 points
191
+ const d = 2 * (x1 * (ym - y2) + xm * (y2 - y1) + x2 * (y1 - ym));
192
+ if (Math.abs(d) < 1e-12) {
193
+ // Degenerate (collinear): return a line
194
+ return makeLine2d(x1, y1, x2, y2);
195
+ }
196
+ const cx = ((x1 * x1 + y1 * y1) * (ym - y2) +
197
+ (xm * xm + ym * ym) * (y2 - y1) +
198
+ (x2 * x2 + y2 * y2) * (y1 - ym)) /
199
+ d;
200
+ const cy = ((x1 * x1 + y1 * y1) * (x2 - xm) +
201
+ (xm * xm + ym * ym) * (x1 - x2) +
202
+ (x2 * x2 + y2 * y2) * (xm - x1)) /
203
+ d;
204
+ const radius = Math.sqrt((x1 - cx) ** 2 + (y1 - cy) ** 2);
205
+ // Compute angles for start (p1), mid (pm), and end (p2)
206
+ const a1 = Math.atan2(y1 - cy, x1 - cx);
207
+ const am = Math.atan2(ym - cy, xm - cx);
208
+ const a2 = Math.atan2(y2 - cy, x2 - cx);
209
+ // Determine sense: CCW if mid-point angle is between start and end going CCW
210
+ let da1m = am - a1;
211
+ if (da1m < 0)
212
+ da1m += 2 * Math.PI;
213
+ let da12 = a2 - a1;
214
+ if (da12 < 0)
215
+ da12 += 2 * Math.PI;
216
+ const sense = da1m < da12; // CCW if midpoint comes before endpoint
217
+ const circle = makeCircle2d(cx, cy, radius, sense);
218
+ if (!sense) {
219
+ // CW circle evaluates angle = -t, so parameter t = -angle.
220
+ const tStart = -a1;
221
+ let tEnd = -a2;
222
+ if (tEnd < tStart - 1e-9)
223
+ tEnd += 2 * Math.PI;
224
+ return { kind2d: 'trimmed', basis: circle, tStart, tEnd };
225
+ }
226
+ // CCW: ensure tEnd >= tStart
227
+ let tEnd = a2;
228
+ if (tEnd < a1 - 1e-9)
229
+ tEnd += 2 * Math.PI;
230
+ return { kind2d: 'trimmed', basis: circle, tStart: a1, tEnd };
231
+ }
232
+ /**
233
+ * Arc from a start point through a given tangent direction to an end point.
234
+ * @param sx - the x coordinate of the start point.
235
+ * @param sy - the y coordinate of the start point.
236
+ * @param tx - the x component of the tangent direction at the start.
237
+ * @param ty - the y component of the tangent direction at the start.
238
+ * @param ex - the x coordinate of the end point.
239
+ * @param ey - the y coordinate of the end point.
240
+ * @returns a Curve2dObj: a trimmed circle arc, or a line when degenerate.
241
+ */
242
+ export function makeArc2dTangent(sx, sy, tx, ty, ex, ey) {
243
+ const len = Math.sqrt(tx * tx + ty * ty);
244
+ const ntx = len > 0 ? tx / len : 0;
245
+ const nty = len > 0 ? ty / len : 0;
246
+ const dx = sx - ex;
247
+ const dy = sy - ey;
248
+ const denom = 2 * (dy * ntx - dx * nty);
249
+ if (Math.abs(denom) < 1e-12) {
250
+ return makeLine2d(sx, sy, ex, ey);
251
+ }
252
+ const chord2 = dx * dx + dy * dy;
253
+ const t = -chord2 / denom;
254
+ const cx = sx - t * nty;
255
+ const cy = sy + t * ntx;
256
+ const radius = Math.abs(t);
257
+ const a1 = Math.atan2(sy - cy, sx - cx);
258
+ const a2 = Math.atan2(ey - cy, ex - cx);
259
+ const ccwTanX = -(sy - cy) / radius;
260
+ const ccwTanY = (sx - cx) / radius;
261
+ const dotCcw = ntx * ccwTanX + nty * ccwTanY;
262
+ let aMid;
263
+ if (dotCcw > 0) {
264
+ let da = a2 - a1;
265
+ if (da <= 0)
266
+ da += 2 * Math.PI;
267
+ aMid = a1 + da / 2;
268
+ }
269
+ else {
270
+ let da = a2 - a1;
271
+ if (da >= 0)
272
+ da -= 2 * Math.PI;
273
+ aMid = a1 + da / 2;
274
+ }
275
+ const mx = cx + radius * Math.cos(aMid);
276
+ const my = cy + radius * Math.sin(aMid);
277
+ return makeArc2dThreePoints(sx, sy, mx, my, ex, ey);
278
+ }
279
+ /**
280
+ * Ellipse with the given center, radii, major-axis direction and sense.
281
+ * @param cx - the x coordinate of the center.
282
+ * @param cy - the y coordinate of the center.
283
+ * @param majorRadius - the semi-major radius.
284
+ * @param minorRadius - the semi-minor radius.
285
+ * @param xDirX - the x component of the major-axis direction (default 1).
286
+ * @param xDirY - the y component of the major-axis direction (default 0).
287
+ * @param sense - true for counter-clockwise winding (default true).
288
+ * @returns an Ellipse2d.
289
+ */
290
+ export function makeEllipse2d(cx, cy, majorRadius, minorRadius, xDirX = 1, xDirY = 0, sense = true) {
291
+ return {
292
+ kind2d: 'ellipse',
293
+ cx,
294
+ cy,
295
+ majorRadius,
296
+ minorRadius,
297
+ xDirAngle: Math.atan2(xDirY, xDirX),
298
+ sense,
299
+ };
300
+ }
301
+ /**
302
+ * Bezier curve from control poles.
303
+ * @param poles - the ordered control points `[x, y]` for the curve.
304
+ * @returns a Bezier2d holding a copy of the poles.
305
+ */
306
+ export function makeBezier2d(poles) {
307
+ return { kind2d: 'bezier', poles: [...poles] };
308
+ }
309
+ // ---------------------------------------------------------------------------
310
+ // Transform helpers
311
+ // ---------------------------------------------------------------------------
312
+ /**
313
+ * Translate a curve by an offset.
314
+ * @param c - the curve to translate.
315
+ * @param dx - the x offset.
316
+ * @param dy - the y offset.
317
+ * @returns a new Curve2dObj translated by `(dx, dy)`.
318
+ */
319
+ export function translateCurve2d(c, dx, dy) {
320
+ switch (c.kind2d) {
321
+ case 'line':
322
+ return { ...c, ox: c.ox + dx, oy: c.oy + dy };
323
+ case 'circle':
324
+ return { ...c, cx: c.cx + dx, cy: c.cy + dy };
325
+ case 'ellipse':
326
+ return { ...c, cx: c.cx + dx, cy: c.cy + dy };
327
+ case 'bezier':
328
+ return { ...c, poles: c.poles.map(([x, y]) => [x + dx, y + dy]) };
329
+ case 'bspline':
330
+ return { ...c, poles: c.poles.map(([x, y]) => [x + dx, y + dy]) };
331
+ case 'trimmed':
332
+ return { ...c, basis: translateCurve2d(c.basis, dx, dy) };
333
+ }
334
+ }
335
+ /**
336
+ * Rotate a curve by an angle about a center point.
337
+ * @param c - the curve to rotate.
338
+ * @param angle - the rotation angle in radians.
339
+ * @param cx - the x coordinate of the rotation center.
340
+ * @param cy - the y coordinate of the rotation center.
341
+ * @returns a new Curve2dObj rotated by `angle` about `(cx, cy)`.
342
+ */
343
+ export function rotateCurve2d(c, angle, cx, cy) {
344
+ const cos = Math.cos(angle);
345
+ const sin = Math.sin(angle);
346
+ const rotatePoint = (x, y) => {
347
+ const rx = x - cx;
348
+ const ry = y - cy;
349
+ return [cx + rx * cos - ry * sin, cy + rx * sin + ry * cos];
350
+ };
351
+ switch (c.kind2d) {
352
+ case 'line': {
353
+ const [ox, oy] = rotatePoint(c.ox, c.oy);
354
+ const ndx = c.dx * cos - c.dy * sin;
355
+ const ndy = c.dx * sin + c.dy * cos;
356
+ return { ...c, ox, oy, dx: ndx, dy: ndy };
357
+ }
358
+ case 'circle': {
359
+ const [ncx, ncy] = rotatePoint(c.cx, c.cy);
360
+ return { ...c, cx: ncx, cy: ncy };
361
+ }
362
+ case 'ellipse': {
363
+ const [ncx, ncy] = rotatePoint(c.cx, c.cy);
364
+ return { ...c, cx: ncx, cy: ncy, xDirAngle: c.xDirAngle + angle };
365
+ }
366
+ case 'bezier':
367
+ return { ...c, poles: c.poles.map(([x, y]) => rotatePoint(x, y)) };
368
+ case 'bspline':
369
+ return { ...c, poles: c.poles.map(([x, y]) => rotatePoint(x, y)) };
370
+ case 'trimmed': {
371
+ const basis = rotateCurve2d(c.basis, angle, cx, cy);
372
+ // A circle carries no frame angle, so its rotation must land in the
373
+ // trim range: parameter IS the geometric angle (negated when
374
+ // sense=false). Without this shift, rotating an arc about its own
375
+ // circle center was a silent no-op.
376
+ if (basis.kind2d === 'circle') {
377
+ const shift = basis.sense ? angle : -angle;
378
+ return { ...c, basis, tStart: c.tStart + shift, tEnd: c.tEnd + shift };
379
+ }
380
+ return { ...c, basis };
381
+ }
382
+ }
383
+ }
384
+ /**
385
+ * Scale a curve by a factor about a center point.
386
+ * @param c - the curve to scale.
387
+ * @param factor - the scale factor (may be negative to mirror-and-scale).
388
+ * @param cx - the x coordinate of the scaling center.
389
+ * @param cy - the y coordinate of the scaling center.
390
+ * @returns a new Curve2dObj scaled by `factor` about `(cx, cy)`.
391
+ */
392
+ export function scaleCurve2d(c, factor, cx, cy) {
393
+ const scalePoint = (x, y) => [
394
+ cx + (x - cx) * factor,
395
+ cy + (y - cy) * factor,
396
+ ];
397
+ switch (c.kind2d) {
398
+ case 'line': {
399
+ // Scale BOTH endpoints and recompute direction/length (see brepjs notes:
400
+ // scaling only origin left the endpoint wrong for factor != 1, most
401
+ // damagingly for mirror factor=-1 which reflected the origin but kept the
402
+ // original direction).
403
+ const [ox, oy] = scalePoint(c.ox, c.oy);
404
+ const [ex, ey] = scalePoint(c.ox + c.dx * c.len, c.oy + c.dy * c.len);
405
+ const ndx = ex - ox;
406
+ const ndy = ey - oy;
407
+ const nlen = Math.sqrt(ndx * ndx + ndy * ndy);
408
+ return {
409
+ ...c,
410
+ ox,
411
+ oy,
412
+ dx: nlen > 0 ? ndx / nlen : c.dx,
413
+ dy: nlen > 0 ? ndy / nlen : c.dy,
414
+ len: nlen,
415
+ };
416
+ }
417
+ case 'circle': {
418
+ const [ncx, ncy] = scalePoint(c.cx, c.cy);
419
+ return { ...c, cx: ncx, cy: ncy, radius: c.radius * Math.abs(factor) };
420
+ }
421
+ case 'ellipse': {
422
+ const [ncx, ncy] = scalePoint(c.cx, c.cy);
423
+ return {
424
+ ...c,
425
+ cx: ncx,
426
+ cy: ncy,
427
+ majorRadius: c.majorRadius * Math.abs(factor),
428
+ minorRadius: c.minorRadius * Math.abs(factor),
429
+ };
430
+ }
431
+ case 'bezier':
432
+ return { ...c, poles: c.poles.map(([x, y]) => scalePoint(x, y)) };
433
+ case 'bspline':
434
+ return { ...c, poles: c.poles.map(([x, y]) => scalePoint(x, y)) };
435
+ case 'trimmed':
436
+ return { ...c, basis: scaleCurve2d(c.basis, factor, cx, cy) };
437
+ }
438
+ }
439
+ /**
440
+ * Mirror a curve through a center point (a half-turn).
441
+ * @param c - the curve to mirror.
442
+ * @param cx - the x coordinate of the mirror center.
443
+ * @param cy - the y coordinate of the mirror center.
444
+ * @returns a new Curve2dObj rotated 180 degrees about `(cx, cy)`.
445
+ */
446
+ export function mirrorAtPoint(c, cx, cy) {
447
+ // A point mirror IS a half-turn. Routing through rotate keeps every curve
448
+ // type's parameterization exact: scale(-1) reflected conic centers but left
449
+ // their angular parameterization behind.
450
+ return rotateCurve2d(c, Math.PI, cx, cy);
451
+ }
452
+ /**
453
+ * Mirror a curve across an axis defined by a point and a direction vector.
454
+ * @param c - the curve to mirror.
455
+ * @param ox - the x coordinate of a point on the mirror axis.
456
+ * @param oy - the y coordinate of a point on the mirror axis.
457
+ * @param dx - the x component of the axis direction vector.
458
+ * @param dy - the y component of the axis direction vector.
459
+ * @returns a new Curve2dObj reflected across the given axis.
460
+ */
461
+ export function mirrorAcrossAxis(c, ox, oy, dx, dy) {
462
+ const len = Math.sqrt(dx * dx + dy * dy);
463
+ if (len < 1e-15)
464
+ return c;
465
+ const nx = dx / len;
466
+ const ny = dy / len;
467
+ const reflectPoint = (x, y) => {
468
+ const rx = x - ox;
469
+ const ry = y - oy;
470
+ const dot = rx * nx + ry * ny;
471
+ return [ox + 2 * dot * nx - rx, oy + 2 * dot * ny - ry];
472
+ };
473
+ switch (c.kind2d) {
474
+ case 'line': {
475
+ const [nox, noy] = reflectPoint(c.ox, c.oy);
476
+ const ndx = 2 * (c.dx * nx + c.dy * ny) * nx - c.dx;
477
+ const ndy = 2 * (c.dx * nx + c.dy * ny) * ny - c.dy;
478
+ return { ...c, ox: nox, oy: noy, dx: ndx, dy: ndy };
479
+ }
480
+ case 'circle': {
481
+ const [ncx, ncy] = reflectPoint(c.cx, c.cy);
482
+ return { ...c, cx: ncx, cy: ncy, sense: !c.sense };
483
+ }
484
+ case 'ellipse': {
485
+ const [ncx, ncy] = reflectPoint(c.cx, c.cy);
486
+ // Reflect the major-axis direction angle across the mirror axis
487
+ const cos2 = nx * nx - ny * ny;
488
+ const sin2 = 2 * nx * ny;
489
+ const newAngle = Math.atan2(sin2 * Math.cos(c.xDirAngle) - cos2 * Math.sin(c.xDirAngle), cos2 * Math.cos(c.xDirAngle) + sin2 * Math.sin(c.xDirAngle));
490
+ return { ...c, cx: ncx, cy: ncy, xDirAngle: newAngle, sense: !c.sense };
491
+ }
492
+ case 'bezier':
493
+ return { ...c, poles: c.poles.map(([x, y]) => reflectPoint(x, y)) };
494
+ case 'bspline':
495
+ return { ...c, poles: c.poles.map(([x, y]) => reflectPoint(x, y)) };
496
+ case 'trimmed': {
497
+ const basis = mirrorAcrossAxis(c.basis, ox, oy, dx, dy);
498
+ // Reflection maps a circle's geometric angle theta to 2*phi - theta
499
+ // (phi = axis angle). The sense flip on the basis absorbs the negation;
500
+ // the 2*phi rotation has nowhere to live on a frameless circle, so it
501
+ // lands in the trim range (sign follows the ORIGINAL sense).
502
+ if (c.basis.kind2d === 'circle' && basis.kind2d === 'circle') {
503
+ const phi = Math.atan2(ny, nx);
504
+ const shift = c.basis.sense ? -2 * phi : 2 * phi;
505
+ return { ...c, basis, tStart: c.tStart + shift, tEnd: c.tEnd + shift };
506
+ }
507
+ return { ...c, basis };
508
+ }
509
+ }
510
+ }
511
+ // ---------------------------------------------------------------------------
512
+ // 2D curve–curve intersection
513
+ // ---------------------------------------------------------------------------
514
+ /**
515
+ * Compute intersection points (and overlapping segments) between two 2D curves.
516
+ * Handles analytic cases (line-line, line-circle, circle-circle) and falls back
517
+ * to numerical sampling + Newton refinement for general curves.
518
+ * @param c1 - the first curve.
519
+ * @param c2 - the second curve.
520
+ * @param tolerance - the geometric tolerance used for candidate filtering.
521
+ * @returns the intersection points and any overlapping curve segments.
522
+ */
523
+ export function intersectCurves2dFn(c1, c2, tolerance) {
524
+ const b1 = unwrapCurve(c1);
525
+ const b2 = unwrapCurve(c2);
526
+ // Analytic: line-line
527
+ if (b1.kind2d === 'line' && b2.kind2d === 'line') {
528
+ return intersectLineLine(c1, b1, c2, b2, tolerance);
529
+ }
530
+ // Analytic: line-circle / circle-line
531
+ if (b1.kind2d === 'line' && b2.kind2d === 'circle') {
532
+ return { points: intersectLineCircle(c1, b1, c2, b2, tolerance), segments: [] };
533
+ }
534
+ if (b1.kind2d === 'circle' && b2.kind2d === 'line') {
535
+ return { points: intersectLineCircle(c2, b2, c1, b1, tolerance), segments: [] };
536
+ }
537
+ // Analytic: circle-circle
538
+ if (b1.kind2d === 'circle' && b2.kind2d === 'circle') {
539
+ return { points: intersectCircleCircle(c1, b1, c2, b2, tolerance), segments: [] };
540
+ }
541
+ // General: numerical (with self-intersection handling)
542
+ const isSelf = c1 === c2;
543
+ return numericalIntersect(c1, c2, tolerance, isSelf);
544
+ }
545
+ /**
546
+ * Serialize a curve (JSON round-trip).
547
+ * @param c - the curve to serialize.
548
+ * @returns the curve as a JSON string.
549
+ */
550
+ export function serializeCurve2d(c) {
551
+ return JSON.stringify(c);
552
+ }
553
+ /**
554
+ * Deserialize a curve previously produced by {@link serializeCurve2d}.
555
+ * @param data - the JSON string describing the curve.
556
+ * @returns the reconstructed Curve2dObj.
557
+ */
558
+ export function deserializeCurve2d(data) {
559
+ return JSON.parse(data);
560
+ }
561
+ // ---------------------------------------------------------------------------
562
+ // BBox helpers
563
+ // ---------------------------------------------------------------------------
564
+ /**
565
+ * Create an empty (inverted) curve bounding box.
566
+ * @returns a fresh CurveBBox2d whose mins are +Infinity and maxes are -Infinity.
567
+ */
568
+ export function createCurveBBox2d() {
569
+ return { kind2d_b: true, xMin: Infinity, yMin: Infinity, xMax: -Infinity, yMax: -Infinity };
570
+ }
571
+ /**
572
+ * Expand a curve bounding box to include a curve's extent.
573
+ * Uses analytic extremes for lines and circles/arcs and sampling otherwise.
574
+ * @param bbox - the box to expand in place.
575
+ * @param c - the curve whose extent is incorporated.
576
+ * @param _tol - reserved tolerance parameter (unused).
577
+ */
578
+ export function addCurveToBBox(bbox, c, _tol) {
579
+ const basis = c.kind2d === 'trimmed' ? c.basis : c;
580
+ // Analytic: lines need only 2 endpoints (no sampling)
581
+ if (basis.kind2d === 'line') {
582
+ const bounds = curveBounds(c);
583
+ if (!isFinite(bounds.first) || !isFinite(bounds.last))
584
+ return;
585
+ const [x0, y0] = evaluateCurve2d(c, bounds.first);
586
+ const [x1, y1] = evaluateCurve2d(c, bounds.last);
587
+ bbox.xMin = Math.min(bbox.xMin, x0, x1);
588
+ bbox.yMin = Math.min(bbox.yMin, y0, y1);
589
+ bbox.xMax = Math.max(bbox.xMax, x0, x1);
590
+ bbox.yMax = Math.max(bbox.yMax, y0, y1);
591
+ return;
592
+ }
593
+ // Analytic: circles/arcs — endpoints + axis extremes
594
+ if (basis.kind2d === 'circle') {
595
+ const bounds = curveBounds(c);
596
+ if (!isFinite(bounds.first) || !isFinite(bounds.last))
597
+ return;
598
+ const { cx, cy, radius, sense } = basis;
599
+ const [x0, y0] = evaluateCurve2d(c, bounds.first);
600
+ const [x1, y1] = evaluateCurve2d(c, bounds.last);
601
+ let xMin = Math.min(x0, x1);
602
+ let xMax = Math.max(x0, x1);
603
+ let yMin = Math.min(y0, y1);
604
+ let yMax = Math.max(y0, y1);
605
+ // Check axis-aligned extreme angles within the arc's parameter range
606
+ const tStart = c.kind2d === 'trimmed' ? c.tStart : bounds.first;
607
+ const tEnd = c.kind2d === 'trimmed' ? c.tEnd : bounds.last;
608
+ for (let k = 0; k < 4; k++) {
609
+ const angle = (k * Math.PI) / 2;
610
+ const t = sense ? angle : -angle;
611
+ // Check if t (or t + 2π) falls within [tStart, tEnd]
612
+ for (let wrap = 0; wrap <= 1; wrap++) {
613
+ const tt = t + wrap * 2 * Math.PI;
614
+ if (tt >= tStart - 1e-10 && tt <= tEnd + 1e-10) {
615
+ const ex = cx + radius * Math.cos(angle);
616
+ const ey = cy + radius * Math.sin(angle);
617
+ xMin = Math.min(xMin, ex);
618
+ xMax = Math.max(xMax, ex);
619
+ yMin = Math.min(yMin, ey);
620
+ yMax = Math.max(yMax, ey);
621
+ break;
622
+ }
623
+ }
624
+ }
625
+ bbox.xMin = Math.min(bbox.xMin, xMin);
626
+ bbox.yMin = Math.min(bbox.yMin, yMin);
627
+ bbox.xMax = Math.max(bbox.xMax, xMax);
628
+ bbox.yMax = Math.max(bbox.yMax, yMax);
629
+ return;
630
+ }
631
+ // General: sample (bezier, bspline, ellipse)
632
+ const bounds = curveBounds(c);
633
+ if (!isFinite(bounds.first) || !isFinite(bounds.last))
634
+ return;
635
+ const nSamples = 20;
636
+ const dt = (bounds.last - bounds.first) / nSamples;
637
+ for (let i = 0; i <= nSamples; i++) {
638
+ const t = bounds.first + i * dt;
639
+ const [x, y] = evaluateCurve2d(c, t);
640
+ if (x < bbox.xMin)
641
+ bbox.xMin = x;
642
+ if (y < bbox.yMin)
643
+ bbox.yMin = y;
644
+ if (x > bbox.xMax)
645
+ bbox.xMax = x;
646
+ if (y > bbox.yMax)
647
+ bbox.yMax = y;
648
+ }
649
+ }
650
+ // ---------------------------------------------------------------------------
651
+ // Private helpers
652
+ // ---------------------------------------------------------------------------
653
+ /** Unwrap trimmed wrappers to get the basis curve type. */
654
+ function unwrapCurve(c) {
655
+ let cur = c;
656
+ while (cur.kind2d === 'trimmed')
657
+ cur = cur.basis;
658
+ return cur;
659
+ }
660
+ /** Check if parameter t is within the curve's domain. */
661
+ function inDomain(c, t, tol) {
662
+ const b = curveBounds(c);
663
+ return t >= b.first - tol && t <= b.last + tol;
664
+ }
665
+ /** Find parameter on curve c closest to point (px, py), searching near tGuess. */
666
+ function refineParam(c, px, py, maxRatio = 0.1) {
667
+ const bounds = curveBounds(c);
668
+ if (!isFinite(bounds.first) || !isFinite(bounds.last))
669
+ return null;
670
+ const N = 80;
671
+ const dt = (bounds.last - bounds.first) / N;
672
+ let bestT = bounds.first;
673
+ let bestD = Infinity;
674
+ for (let i = 0; i <= N; i++) {
675
+ const t = bounds.first + i * dt;
676
+ const [ex, ey] = evaluateCurve2d(c, t);
677
+ const d = (ex - px) ** 2 + (ey - py) ** 2;
678
+ if (d < bestD) {
679
+ bestD = d;
680
+ bestT = t;
681
+ }
682
+ }
683
+ // bestD is squared geometric distance. Derive a scale-relative threshold
684
+ // from the curve's geometric extent (not parameter span).
685
+ const [sx, sy] = evaluateCurve2d(c, bounds.first);
686
+ const [ex, ey] = evaluateCurve2d(c, bounds.last);
687
+ const [mx, my] = evaluateCurve2d(c, (bounds.first + bounds.last) / 2);
688
+ const geomExtent = Math.max(Math.sqrt((ex - sx) ** 2 + (ey - sy) ** 2), Math.sqrt((mx - sx) ** 2 + (my - sy) ** 2), 1e-6);
689
+ const maxDist = geomExtent * maxRatio;
690
+ return bestD < maxDist * maxDist ? bestT : null;
691
+ }
692
+ /**
693
+ * Parameter on a curve closest to a point.
694
+ *
695
+ * This is the general "point → parameter" primitive used by corner/offset work:
696
+ * it searches the curve's domain for the parameter whose evaluated point is
697
+ * nearest to `(px, py)` and returns it when that nearest distance is within a
698
+ * configurable fraction of the curve's geometric extent. Returns `null` when the
699
+ * point lies too far off the curve (the raw gap-vs-extent guard) so callers can
700
+ * distinguish "on/very near the curve" from "clearly off".
701
+ *
702
+ * @param c - the curve to search (line, arc, ellipse, bezier, bspline, trimmed).
703
+ * @param px - the query point x.
704
+ * @param py - the query point y.
705
+ * @param maxRatio - the maximum acceptable nearest-distance / geometric-extent
706
+ * ratio (default 0.1; raise it to accept points that sit further off the curve).
707
+ * @returns the domain parameter `t`, or `null` when `(px, py)` is off-curve by
708
+ * more than `maxRatio` of the curve's geometric extent.
709
+ */
710
+ export function parameterOfPoint(c, px, py, maxRatio = 0.1) {
711
+ const bounds = curveBounds(c);
712
+ if (!isFinite(bounds.first) || !isFinite(bounds.last))
713
+ return null;
714
+ // Coarse sample to get within one domain step of the nearest point (semantics,
715
+ // curvature, and per-kernel sensitivity: `refineParam`'s N=80 grid).
716
+ const seed = refineParam(c, px, py, maxRatio);
717
+ if (seed === null)
718
+ return null;
719
+ // Refine to the projection foot: for the farthest/nearest parameter the chord
720
+ // `(curve(t) − P)` is perpendicular to the tangent, i.e. g(t)=dot(·)=0. Solve
721
+ // g(t)=0 with Newton on a central-difference derivative.
722
+ const span = bounds.last - bounds.first;
723
+ const hbase = span * 1e-6;
724
+ let t = seed;
725
+ const fixed = new Set();
726
+ for (let i = 0; i < 24; i++) {
727
+ const [ex, ey] = evaluateCurve2d(c, t);
728
+ const [tx, ty] = tangentCurve2d(c, t);
729
+ const g = (ex - px) * tx + (ey - py) * ty;
730
+ if (Math.abs(g) < 1e-12)
731
+ break;
732
+ const h = Math.max(hbase, 1e-9);
733
+ const [hx, hy] = evaluateCurve2d(c, Math.min(bounds.last, t + h));
734
+ const [hT, hTy] = tangentCurve2d(c, Math.min(bounds.last, t + h));
735
+ const gp = ((hx - px) * hT + (hy - py) * hTy - g) / h;
736
+ if (Math.abs(gp) < 1e-16)
737
+ break;
738
+ const tn = t - g / gp;
739
+ t = Math.max(bounds.first, Math.min(bounds.last, tn));
740
+ const key = t.toFixed(12);
741
+ if (fixed.has(key))
742
+ break;
743
+ fixed.add(key);
744
+ if (okConverge(c, px, py, t))
745
+ break;
746
+ }
747
+ return t;
748
+ }
749
+ /** True when `(px, py)` is within `1e-9` of the curve at parameter `t`. */
750
+ function okConverge(c, px, py, t) {
751
+ const [ex, ey] = evaluateCurve2d(c, t);
752
+ return Math.hypot(ex - px, ey - py) < 1e-9;
753
+ }
754
+ /** The unwrapped basis curve (strips any nesting of `trimmed` wrappers). */
755
+ function basisOf(c) {
756
+ let b = c;
757
+ while (b.kind2d === 'trimmed')
758
+ b = b.basis;
759
+ return b;
760
+ }
761
+ /**
762
+ * Trim a curve to a sub-parameter range given in the basis curve's native domain.
763
+ *
764
+ * The returned `trimmed` curve keeps the full basis; its `t∈[0,1]` domain maps
765
+ * into `[tStart, tEnd]` of the basis (so it is the inverse of `curveBounds` for
766
+ * trimmed curves). Parameters are in the basis's native domain (a line: distance
767
+ * along `[0,len]`; an arc/ellipse: angle `[.., ..]`).
768
+ *
769
+ * @param c - the curve to trim (may itself be already-trimmed; the basis is reused).
770
+ * @param tStart - the basis-domain parameter where the sub-curve starts.
771
+ * @param tEnd - the basis-domain parameter where the sub-curve ends.
772
+ * @returns the trimmed `Curve2dObj`.
773
+ */
774
+ export function trimCurve(c, tStart, tEnd) {
775
+ return { kind2d: 'trimmed', basis: basisOf(c), tStart, tEnd };
776
+ }
777
+ /**
778
+ * Split a curve at a native-domain parameter into two trimmed sub-curves.
779
+ *
780
+ * This is the splice primitive that D1 boolean-splitting and D3 corner
781
+ * (fillet/chamfer) use: it cuts a single curve at an interior parameter into
782
+ * two `TrimmedCurve2d` sharing the same basis, covering `[first, t]` and
783
+ * `[t, last]`. Returns `null` when `t` is not strictly interior.
784
+ *
785
+ * @param c - the curve to split.
786
+ * @param t - the native-domain split parameter (strictly inside the basis domain).
787
+ * @returns the two sub-curves, or `null` when `t` is outside the interior.
788
+ */
789
+ export function splitCurveAt(c, t) {
790
+ const basis = basisOf(c);
791
+ const { first, last } = curveBounds(basis);
792
+ if (t <= first || t >= last)
793
+ return null;
794
+ return [trimCurve(basis, first, t), trimCurve(basis, t, last)];
795
+ }
796
+ function intersectLineLine(c1, l1, c2, l2, tol) {
797
+ const det = l1.dx * l2.dy - l1.dy * l2.dx;
798
+ if (Math.abs(det) >= 1e-14) {
799
+ // Non-parallel: solve for intersection
800
+ const ex = l2.ox - l1.ox;
801
+ const ey = l2.oy - l1.oy;
802
+ const t1 = (ex * l2.dy - ey * l2.dx) / det;
803
+ const t2 = (ex * l1.dy - ey * l1.dx) / det;
804
+ if (!inDomain(c1, t1, tol) || !inDomain(c2, t2, tol))
805
+ return { points: [], segments: [] };
806
+ return { points: [[l1.ox + t1 * l1.dx, l1.oy + t1 * l1.dy]], segments: [] };
807
+ }
808
+ // Parallel — check if collinear and overlapping
809
+ const ex = l2.ox - l1.ox;
810
+ const ey = l2.oy - l1.oy;
811
+ const cross = ex * l1.dy - ey * l1.dx;
812
+ if (Math.abs(cross) > tol)
813
+ return { points: [], segments: [] }; // parallel but not collinear
814
+ // Project c2 endpoints onto c1's parameter space
815
+ const b1 = curveBounds(c1);
816
+ const b2 = curveBounds(c2);
817
+ const p2s = evaluateCurve2d(c2, b2.first);
818
+ const p2e = evaluateCurve2d(c2, b2.last);
819
+ const t2sOn1 = (p2s[0] - l1.ox) * l1.dx + (p2s[1] - l1.oy) * l1.dy;
820
+ const t2eOn1 = (p2e[0] - l1.ox) * l1.dx + (p2e[1] - l1.oy) * l1.dy;
821
+ const overlapStart = Math.max(b1.first, Math.min(t2sOn1, t2eOn1));
822
+ const overlapEnd = Math.min(b1.last, Math.max(t2sOn1, t2eOn1));
823
+ if (overlapEnd - overlapStart < tol)
824
+ return { points: [], segments: [] }; // no meaningful overlap
825
+ const sx = l1.ox + overlapStart * l1.dx;
826
+ const sy = l1.oy + overlapStart * l1.dy;
827
+ const ex2 = l1.ox + overlapEnd * l1.dx;
828
+ const ey2 = l1.oy + overlapEnd * l1.dy;
829
+ const seg = makeLine2d(sx, sy, ex2, ey2);
830
+ return { points: [], segments: [seg] };
831
+ }
832
+ function intersectLineCircle(cLine, line, cCirc, circ, tol) {
833
+ // Vector from circle center to line origin
834
+ const fx = line.ox - circ.cx;
835
+ const fy = line.oy - circ.cy;
836
+ // Quadratic: |f + t*d|^2 = r^2
837
+ const a = line.dx * line.dx + line.dy * line.dy; // = 1 for normalized
838
+ const b = 2 * (fx * line.dx + fy * line.dy);
839
+ const c = fx * fx + fy * fy - circ.radius * circ.radius;
840
+ const disc = b * b - 4 * a * c;
841
+ if (disc < -tol)
842
+ return [];
843
+ const results = [];
844
+ const sqrtDisc = Math.sqrt(Math.max(0, disc));
845
+ const t1 = (-b - sqrtDisc) / (2 * a);
846
+ const t2 = (-b + sqrtDisc) / (2 * a);
847
+ for (const tLine of disc < tol * tol ? [t1] : [t1, t2]) {
848
+ if (!inDomain(cLine, tLine, tol))
849
+ continue;
850
+ const px = line.ox + tLine * line.dx;
851
+ const py = line.oy + tLine * line.dy;
852
+ // Check that the point is in the circle's domain
853
+ const tCirc = refineParam(cCirc, px, py);
854
+ if (tCirc === null)
855
+ continue;
856
+ const [cx2, cy2] = evaluateCurve2d(cCirc, tCirc);
857
+ if ((cx2 - px) ** 2 + (cy2 - py) ** 2 > tol * tol * 1e6)
858
+ continue;
859
+ results.push([px, py]);
860
+ }
861
+ return results;
862
+ }
863
+ function intersectConcentricArcs(c1, c2, tol) {
864
+ const b1 = curveBounds(c1);
865
+ const b2 = curveBounds(c2);
866
+ const isFullCircle1 = Math.abs(b1.last - b1.first - 2 * Math.PI) < 1e-10;
867
+ const isFullCircle2 = Math.abs(b2.last - b2.first - 2 * Math.PI) < 1e-10;
868
+ if (isFullCircle1 && isFullCircle2)
869
+ return [];
870
+ if (isFullCircle1) {
871
+ return [evaluateCurve2d(c2, b2.first), evaluateCurve2d(c2, b2.last)];
872
+ }
873
+ if (isFullCircle2) {
874
+ return [evaluateCurve2d(c1, b1.first), evaluateCurve2d(c1, b1.last)];
875
+ }
876
+ const pts = [];
877
+ const checks = [
878
+ [c2, evaluateCurve2d(c1, b1.first)],
879
+ [c2, evaluateCurve2d(c1, b1.last)],
880
+ [c1, evaluateCurve2d(c2, b2.first)],
881
+ [c1, evaluateCurve2d(c2, b2.last)],
882
+ ];
883
+ for (const [target, pt] of checks) {
884
+ const t = refineParam(target, pt[0], pt[1]);
885
+ if (t !== null) {
886
+ const [ex, ey] = evaluateCurve2d(target, t);
887
+ if ((ex - pt[0]) ** 2 + (ey - pt[1]) ** 2 < tol * tol * 100)
888
+ pts.push(pt);
889
+ }
890
+ }
891
+ const deduped = [];
892
+ for (const p of pts) {
893
+ if (!deduped.some(([ddx, ddy]) => (ddx - p[0]) ** 2 + (ddy - p[1]) ** 2 < tol * tol * 100)) {
894
+ deduped.push(p);
895
+ }
896
+ }
897
+ return deduped;
898
+ }
899
+ function intersectCircleCircle(c1, circ1, c2, circ2, tol) {
900
+ const dx = circ2.cx - circ1.cx;
901
+ const dy = circ2.cy - circ1.cy;
902
+ const d = Math.sqrt(dx * dx + dy * dy);
903
+ if (d > circ1.radius + circ2.radius + tol)
904
+ return [];
905
+ if (d < Math.abs(circ1.radius - circ2.radius) - tol)
906
+ return [];
907
+ if (d < 1e-14) {
908
+ if (Math.abs(circ1.radius - circ2.radius) > tol)
909
+ return [];
910
+ return intersectConcentricArcs(c1, c2, tol);
911
+ }
912
+ const a = (circ1.radius * circ1.radius - circ2.radius * circ2.radius + d * d) / (2 * d);
913
+ const h2 = circ1.radius * circ1.radius - a * a;
914
+ const h = Math.sqrt(Math.max(0, h2));
915
+ const mx = circ1.cx + (a * dx) / d;
916
+ const my = circ1.cy + (a * dy) / d;
917
+ const candidates = h < tol
918
+ ? [[mx, my]]
919
+ : [
920
+ [mx + (h * dy) / d, my - (h * dx) / d],
921
+ [mx - (h * dy) / d, my + (h * dx) / d],
922
+ ];
923
+ // Filter candidates that lie within both curves' domains
924
+ const results = [];
925
+ for (const [px, py] of candidates) {
926
+ const t1 = refineParam(c1, px, py);
927
+ const t2 = refineParam(c2, px, py);
928
+ if (t1 === null || t2 === null)
929
+ continue;
930
+ const [x1, y1] = evaluateCurve2d(c1, t1);
931
+ const [x2, y2] = evaluateCurve2d(c2, t2);
932
+ const tolSq = (tol * 10) ** 2;
933
+ if ((x1 - px) ** 2 + (y1 - py) ** 2 > tolSq)
934
+ continue;
935
+ if ((x2 - px) ** 2 + (y2 - py) ** 2 > tolSq)
936
+ continue;
937
+ results.push([px, py]);
938
+ }
939
+ return results;
940
+ }
941
+ /**
942
+ * Numerical intersection via sampling + Newton refinement.
943
+ * Samples both curves densely, finds close point pairs, refines with Newton.
944
+ */
945
+ function numericalIntersect(c1, c2, tolerance, isSelf = false) {
946
+ const b1 = curveBounds(c1);
947
+ const b2 = curveBounds(c2);
948
+ if (!isFinite(b1.first) || !isFinite(b1.last) || !isFinite(b2.first) || !isFinite(b2.last)) {
949
+ return { points: [], segments: [] };
950
+ }
951
+ const N = 100;
952
+ const pts1 = [];
953
+ const pts2 = [];
954
+ for (let i = 0; i <= N; i++) {
955
+ const t1 = b1.first + ((b1.last - b1.first) * i) / N;
956
+ const [x1, y1] = evaluateCurve2d(c1, t1);
957
+ pts1.push({ t: t1, x: x1, y: y1 });
958
+ const t2 = b2.first + ((b2.last - b2.first) * i) / N;
959
+ const [x2, y2] = evaluateCurve2d(c2, t2);
960
+ pts2.push({ t: t2, x: x2, y: y2 });
961
+ }
962
+ // Pre-compute segment bounds for curve2 (avoids recomputing N times in inner loop)
963
+ const crossTol = Math.max(tolerance * 100, 0.5);
964
+ const seg2Bounds = new Float64Array(N * 6); // xmin, xmax, ymin, ymax, tmid, _pad
965
+ for (let j = 0; j < N; j++) {
966
+ const a = pts2[j];
967
+ const b = pts2[j + 1];
968
+ const off = j * 6;
969
+ seg2Bounds[off] = Math.min(a.x, b.x);
970
+ seg2Bounds[off + 1] = Math.max(a.x, b.x);
971
+ seg2Bounds[off + 2] = Math.min(a.y, b.y);
972
+ seg2Bounds[off + 3] = Math.max(a.y, b.y);
973
+ seg2Bounds[off + 4] = (a.t + b.t) / 2;
974
+ }
975
+ // Find segment pairs where the curves are close
976
+ const candidates = [];
977
+ const selfMinSep = (b1.last - b1.first) / 5;
978
+ for (let i = 0; i < N; i++) {
979
+ const p1a = pts1[i];
980
+ const p1b = pts1[i + 1];
981
+ // Hoist outer segment AABB (computed once per outer iteration)
982
+ const x1min = Math.min(p1a.x, p1b.x) - crossTol;
983
+ const x1max = Math.max(p1a.x, p1b.x) + crossTol;
984
+ const y1min = Math.min(p1a.y, p1b.y) - crossTol;
985
+ const y1max = Math.max(p1a.y, p1b.y) + crossTol;
986
+ const t1mid = (p1a.t + p1b.t) / 2;
987
+ for (let j = 0; j < N; j++) {
988
+ const off = j * 6;
989
+ if (x1max < seg2Bounds[off] || seg2Bounds[off + 1] < x1min)
990
+ continue;
991
+ if (y1max < seg2Bounds[off + 2] || seg2Bounds[off + 3] < y1min)
992
+ continue;
993
+ const t2mid = seg2Bounds[off + 4];
994
+ if (isSelf && Math.abs(t1mid - t2mid) < selfMinSep)
995
+ continue;
996
+ candidates.push({ t1: t1mid, t2: t2mid });
997
+ }
998
+ }
999
+ // Refine candidates via Newton iteration
1000
+ const tol2 = tolerance * tolerance;
1001
+ const found = [];
1002
+ for (const { t1: t1Init, t2: t2Init } of candidates) {
1003
+ let t1 = t1Init;
1004
+ let t2 = t2Init;
1005
+ for (let iter = 0; iter < 20; iter++) {
1006
+ const [x1, y1] = evaluateCurve2d(c1, t1);
1007
+ const [x2, y2] = evaluateCurve2d(c2, t2);
1008
+ const dx = x1 - x2;
1009
+ const dy = y1 - y2;
1010
+ if (dx * dx + dy * dy < tol2)
1011
+ break;
1012
+ const d1 = tangentCurve2d(c1, t1);
1013
+ const d2 = tangentCurve2d(c2, t2);
1014
+ // Newton system: J * [dt1, dt2]^T = -[dx, dy]
1015
+ const det = d1[0] * -d2[1] - -d2[0] * d1[1];
1016
+ if (Math.abs(det) < 1e-14)
1017
+ break;
1018
+ const dt1 = (-dx * -d2[1] - -dy * -d2[0]) / det;
1019
+ const dt2 = (d1[0] * -dy - d1[1] * -dx) / det;
1020
+ t1 += dt1;
1021
+ t2 += dt2;
1022
+ t1 = Math.max(b1.first, Math.min(b1.last, t1));
1023
+ t2 = Math.max(b2.first, Math.min(b2.last, t2));
1024
+ }
1025
+ const [x1, y1] = evaluateCurve2d(c1, t1);
1026
+ const [x2, y2] = evaluateCurve2d(c2, t2);
1027
+ // For self-intersection, only accept points with well-separated params
1028
+ if (isSelf && Math.abs(t1 - t2) < (b1.last - b1.first) * 0.05)
1029
+ continue;
1030
+ if ((x1 - x2) ** 2 + (y1 - y2) ** 2 < tolerance * tolerance * 1e6) {
1031
+ const px = (x1 + x2) / 2;
1032
+ const py = (y1 + y2) / 2;
1033
+ let dup = false;
1034
+ for (const [fx, fy] of found) {
1035
+ if ((fx - px) ** 2 + (fy - py) ** 2 < tolerance * tolerance * 1e4) {
1036
+ dup = true;
1037
+ break;
1038
+ }
1039
+ }
1040
+ if (!dup)
1041
+ found.push([px, py]);
1042
+ }
1043
+ }
1044
+ return { points: found, segments: [] };
1045
+ }
1046
+ // ---------------------------------------------------------------------------
1047
+ // Private curve evaluators
1048
+ // ---------------------------------------------------------------------------
1049
+ function evaluateBezier(poles, t) {
1050
+ // De Casteljau
1051
+ const n = poles.length;
1052
+ const work = poles.map(([x, y]) => [x, y]);
1053
+ for (let r = 1; r < n; r++) {
1054
+ for (let i = 0; i < n - r; i++) {
1055
+ const wi = work[i];
1056
+ const wi1 = work[i + 1];
1057
+ wi[0] = (1 - t) * wi[0] + t * wi1[0];
1058
+ wi[1] = (1 - t) * wi[1] + t * wi1[1];
1059
+ }
1060
+ }
1061
+ return work[0];
1062
+ }
1063
+ function evaluateBSpline2d(c, t) {
1064
+ // Expand knots with multiplicities
1065
+ const fullKnots = [];
1066
+ for (let i = 0; i < c.knots.length; i++) {
1067
+ const mult = c.multiplicities[i] ?? 1;
1068
+ for (let j = 0; j < mult; j++) {
1069
+ fullKnots.push(c.knots[i]);
1070
+ }
1071
+ }
1072
+ // De Boor evaluation
1073
+ const p = c.degree;
1074
+ const n = c.poles.length;
1075
+ const k = fullKnots.length;
1076
+ const tClamped = Math.max(fullKnots[p], Math.min(fullKnots[k - p - 1], t));
1077
+ // Find span
1078
+ let span = p;
1079
+ for (let i = p; i < k - p - 1; i++) {
1080
+ if (tClamped >= fullKnots[i] && tClamped < fullKnots[i + 1]) {
1081
+ span = i;
1082
+ break;
1083
+ }
1084
+ }
1085
+ if (tClamped >= fullKnots[k - p - 1])
1086
+ span = k - p - 2;
1087
+ // Extract relevant control points
1088
+ const d = [];
1089
+ for (let j = 0; j <= p; j++) {
1090
+ const idx = Math.min(span - p + j, n - 1);
1091
+ const pole = c.poles[Math.max(0, idx)];
1092
+ d.push([pole[0], pole[1]]);
1093
+ }
1094
+ // De Boor recursion
1095
+ for (let r = 1; r <= p; r++) {
1096
+ for (let j = p; j >= r; j--) {
1097
+ const i = span - p + j;
1098
+ const left = fullKnots[i] ?? 0;
1099
+ const right = fullKnots[i + p - r + 1] ?? 1;
1100
+ const denom = right - left;
1101
+ const alpha = denom > 1e-15 ? (tClamped - left) / denom : 0;
1102
+ const dj = d[j];
1103
+ const djPrev = d[j - 1];
1104
+ dj[0] = (1 - alpha) * djPrev[0] + alpha * dj[0];
1105
+ dj[1] = (1 - alpha) * djPrev[1] + alpha * dj[1];
1106
+ }
1107
+ }
1108
+ return d[p];
1109
+ }
1110
+ //# sourceMappingURL=curve2d.js.map