@faicad/faijs-sketch 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +28 -0
  2. package/NOTICE +28 -0
  3. package/dist/bspline.d.ts +58 -0
  4. package/dist/bspline.d.ts.map +1 -0
  5. package/dist/bspline.js +93 -0
  6. package/dist/bspline.js.map +1 -0
  7. package/dist/canonical.d.ts +205 -0
  8. package/dist/canonical.d.ts.map +1 -0
  9. package/dist/canonical.js +12 -0
  10. package/dist/canonical.js.map +1 -0
  11. package/dist/contour.d.ts +43 -0
  12. package/dist/contour.d.ts.map +1 -0
  13. package/dist/contour.js +132 -0
  14. package/dist/contour.js.map +1 -0
  15. package/dist/faces.d.ts +28 -0
  16. package/dist/faces.d.ts.map +1 -0
  17. package/dist/faces.js +46 -0
  18. package/dist/faces.js.map +1 -0
  19. package/dist/fcstd-types.d.ts +177 -0
  20. package/dist/fcstd-types.d.ts.map +1 -0
  21. package/dist/fcstd-types.js +67 -0
  22. package/dist/fcstd-types.js.map +1 -0
  23. package/dist/index.d.ts +36 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +28 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/namespace.d.ts +51 -0
  28. package/dist/namespace.d.ts.map +1 -0
  29. package/dist/namespace.js +67 -0
  30. package/dist/namespace.js.map +1 -0
  31. package/dist/node.d.ts +14 -0
  32. package/dist/node.d.ts.map +1 -0
  33. package/dist/node.js +34 -0
  34. package/dist/node.js.map +1 -0
  35. package/dist/op.d.ts +67 -0
  36. package/dist/op.d.ts.map +1 -0
  37. package/dist/op.js +104 -0
  38. package/dist/op.js.map +1 -0
  39. package/dist/planegcs-backend.d.ts +46 -0
  40. package/dist/planegcs-backend.d.ts.map +1 -0
  41. package/dist/planegcs-backend.js +696 -0
  42. package/dist/planegcs-backend.js.map +1 -0
  43. package/dist/project.d.ts +115 -0
  44. package/dist/project.d.ts.map +1 -0
  45. package/dist/project.js +329 -0
  46. package/dist/project.js.map +1 -0
  47. package/dist/solve.d.ts +21 -0
  48. package/dist/solve.d.ts.map +1 -0
  49. package/dist/solve.js +102 -0
  50. package/dist/solve.js.map +1 -0
  51. package/dist/solver.d.ts +63 -0
  52. package/dist/solver.d.ts.map +1 -0
  53. package/dist/solver.js +28 -0
  54. package/dist/solver.js.map +1 -0
  55. package/dist/verify.d.ts +53 -0
  56. package/dist/verify.d.ts.map +1 -0
  57. package/dist/verify.js +71 -0
  58. package/dist/verify.js.map +1 -0
  59. package/package.json +65 -0
@@ -0,0 +1,696 @@
1
+ /**
2
+ * M3.2/M3.3/M3.4 — planegcs WASM backend for the SketchSolver interface.
3
+ *
4
+ * M3.3: geometry → GCS primitives, including the implicit fixed RtPnt/HAxis/
5
+ * VAxis (geoId -1/-2, plan §5.4). External geometry (geoId <= -3) is rejected
6
+ * upstream (D4: whole sketch downgrades to L2).
7
+ * M3.4: constraints → GCS primitives for the P0 set (plan §7 M3.4).
8
+ * M3.5 helper: solved params are pulled back and geometry rebuilt, so the
9
+ * caller can compare against on-disk coordinates (D2).
10
+ */
11
+ import { make_gcs_wrapper, Algorithm } from '@salusoft89/planegcs';
12
+ import { ok } from '@faicad/faijs/api/result';
13
+ import { ConstraintType, PointPos } from './fcstd-types.js';
14
+ import { SUPPORTED_CONSTRAINT_TYPES } from './solver.js';
15
+ /**
16
+ * Instantiate the planegcs WASM solver.
17
+ *
18
+ * Environment-agnostic: the host supplies the wasm source. Node hosts use
19
+ * `createNodePlanegcsSolver()` from `@faicad/faijs-sketch/node`; browser hosts
20
+ * pass bytes resolved through `HostPorts.assets`.
21
+ *
22
+ * @param options - wasm path (Node) or bytes (browser).
23
+ * @returns a `SketchSolver` backed by the planegcs WASM module.
24
+ */
25
+ export async function createPlanegcsSolver(options) {
26
+ const source = options.wasmBytes ?? options.wasmPath;
27
+ if (!source) {
28
+ throw new Error('E_SKETCHC_NO_WASM: createPlanegcsSolver needs wasmPath or wasmBytes');
29
+ }
30
+ const wrapper = await make_gcs_wrapper(source);
31
+ return new PlanegcsSolver(wrapper);
32
+ }
33
+ const P = (geoId, pos) => `g${geoId}p${pos}`;
34
+ /**
35
+ * planegcs WASM implementation of the `SketchSolver` interface (M3.2–M3.4):
36
+ * pushes geometry/constraints into the GCS, solves, and pulls the solved
37
+ * parameters back into `FcstdSketchGeom` values.
38
+ */
39
+ export class PlanegcsSolver {
40
+ wrapper;
41
+ constructor(wrapper) {
42
+ this.wrapper = wrapper;
43
+ }
44
+ async solve(geoms, constraints, external) {
45
+ const externalMap = new Map((external ?? []).map((e) => [e.geoId, e]));
46
+ // a constraint referencing an unresolvable external geoId is dropped
47
+ const unresolvableExternal = new Set();
48
+ for (const c of constraints) {
49
+ for (const r of c.refs) {
50
+ if (r.geoId <= -3 && r.geoId > -2000 && !externalMap.has(r.geoId)) {
51
+ unresolvableExternal.add(c.index);
52
+ }
53
+ }
54
+ }
55
+ const usableConstraints = constraints.filter((c) => !unresolvableExternal.has(c.index));
56
+ // P3-2 (2026-09-24): constraint types that do NOT affect the solved
57
+ // geometry are dropped-and-recorded instead of failing the whole sketch
58
+ // (previously any of these baked the sketch as unsupported-constraint):
59
+ // - 15 InternalAlignment: alignment bookkeeping between element pairs;
60
+ // the aligned geometry itself is already in <Geometry> — the alignment
61
+ // constraint adds no independent DoF removal for our solving purpose.
62
+ // - 17 Block: freezes a geometry's current position — the stored
63
+ // geometry IS that position, so dropping it changes nothing.
64
+ // - 19 Weight: B-Spline control-point weight — we do not solve splines.
65
+ const ignorable = constraints.filter((c) => c.type === 15 || c.type === 17 || c.type === 19).map((c) => c.index);
66
+ const ignorableSet = new Set(ignorable);
67
+ const unsupported = usableConstraints.find((c) => !SUPPORTED_CONSTRAINT_TYPES.has(c.type) && !ignorableSet.has(c.index));
68
+ if (unsupported) {
69
+ return ok({
70
+ geoms,
71
+ converged: false,
72
+ reason: 'unsupported-constraint',
73
+ problemConstraints: [unsupported.index],
74
+ droppedConstraints: [...unresolvableExternal, ...ignorable],
75
+ });
76
+ }
77
+ const w = this.wrapper;
78
+ w.clear_data();
79
+ // --- M3.3: implicit fixed frame (root point + H/V axes) ---
80
+ w.push_primitive({ type: 'point', id: P(-1, 1), x: 0, y: 0, fixed: true });
81
+ // GOTCHA (probe-sym-min.ts / probe-l1-ablation.ts): FreeCAD models HAxis/
82
+ // VAxis as real Line entries in Geoms (geoId -1/-2), so constraints may
83
+ // reference axis points (pos 1/2) or the axis edge (pos 0) — e.g.
84
+ // DistanceX(line-point, VAxis) fixes an x coordinate. planegcs has no
85
+ // implicit axes, so we materialize both axes as fixed point+line pairs
86
+ // and register them in `lines` (below) so all constraint shapes see them.
87
+ w.push_primitive({ type: 'point', id: P(-1, 2), x: 1, y: 0, fixed: true });
88
+ w.push_primitive({ type: 'point', id: P(-2, 1), x: 0, y: 0, fixed: true });
89
+ w.push_primitive({ type: 'point', id: P(-2, 2), x: 0, y: 1, fixed: true });
90
+ w.push_primitive({ type: 'line', id: 'L-1', p1_id: P(-1, 1), p2_id: P(-1, 2) });
91
+ w.push_primitive({ type: 'line', id: 'L-2', p1_id: P(-2, 1), p2_id: P(-2, 2) });
92
+ // --- M6.3: external fixed geometry (geoId -3, -4, ... in link order) ---
93
+ const externalLines = new Map();
94
+ for (const ext of external ?? []) {
95
+ if (ext.polyline.length === 2) {
96
+ // straight segment: two fixed endpoints + fixed line
97
+ const p1 = P(ext.geoId, 1);
98
+ const p2 = P(ext.geoId, 2);
99
+ w.push_primitive({ type: 'point', id: p1, x: ext.polyline[0][0], y: ext.polyline[0][1], fixed: true });
100
+ w.push_primitive({ type: 'point', id: p2, x: ext.polyline[1][0], y: ext.polyline[1][1], fixed: true });
101
+ w.push_primitive({ type: 'line', id: `X${ext.geoId}`, p1_id: p1, p2_id: p2 });
102
+ externalLines.set(ext.geoId, { p1, p2 });
103
+ }
104
+ else {
105
+ // multi-point polyline: pin sampled points as fixed point targets
106
+ ext.polyline.forEach((pt, i) => {
107
+ w.push_primitive({
108
+ type: 'point', id: P(ext.geoId, i + 1), x: pt[0], y: pt[1], fixed: true,
109
+ });
110
+ });
111
+ }
112
+ }
113
+ // --- M3.3: geometry → primitives ---
114
+ // Track which point keys exist as standalone points vs derived from lines.
115
+ const lines = new Map();
116
+ const arcs = new Map();
117
+ const circles = new Map();
118
+ const ellipses = new Map();
119
+ const standalone = new Map();
120
+ // implicit axes are first-class lines for constraint resolution (see
121
+ // the GOTCHA block above): geoId -1 = HAxis, -2 = VAxis
122
+ lines.set(-1, { p1: P(-1, 1), p2: P(-1, 2) });
123
+ lines.set(-2, { p1: P(-2, 1), p2: P(-2, 2) });
124
+ // pass 1: standalone points that are constraint anchors
125
+ const anchorNeedsStandalone = new Set();
126
+ for (const c of constraints) {
127
+ for (const r of c.refs) {
128
+ if (r.geoId >= 0 && r.pos !== PointPos.none)
129
+ anchorNeedsStandalone.add(P(r.geoId, r.pos));
130
+ }
131
+ }
132
+ for (const g of geoms) {
133
+ switch (g.kind) {
134
+ case 'point': {
135
+ const id = P(g.index, 1);
136
+ standalone.set(g.index, id);
137
+ w.push_primitive({ type: 'point', id, x: g.x, y: g.y, fixed: false });
138
+ break;
139
+ }
140
+ default:
141
+ break;
142
+ }
143
+ }
144
+ // pass 2: curves; create endpoint points for lines/arcs, center points
145
+ for (const g of geoms) {
146
+ switch (g.kind) {
147
+ case 'line': {
148
+ const p1 = anchorNeedsStandalone.has(P(g.index, 1)) ? P(g.index, 1) : P(g.index, 1);
149
+ const p2 = P(g.index, 2);
150
+ // create endpoint points as real GCS points, then a line over them
151
+ for (const [id, x, y] of [
152
+ [p1, g.x1, g.y1],
153
+ [p2, g.x2, g.y2],
154
+ ]) {
155
+ w.push_primitive({ type: 'point', id, x, y, fixed: false });
156
+ }
157
+ w.push_primitive({ type: 'line', id: `L${g.index}`, p1_id: p1, p2_id: p2 });
158
+ lines.set(g.index, { p1, p2 });
159
+ break;
160
+ }
161
+ case 'circle': {
162
+ const c = P(g.index, 3);
163
+ w.push_primitive({ type: 'point', id: c, x: g.cx, y: g.cy, fixed: false });
164
+ w.push_primitive({ type: 'circle', id: `C${g.index}`, c_id: c, radius: g.radius });
165
+ circles.set(g.index, { center: c });
166
+ break;
167
+ }
168
+ case 'arc': {
169
+ const c = P(g.index, 3);
170
+ const s = P(g.index, 1);
171
+ const e = P(g.index, 2);
172
+ for (const [id, x, y] of [
173
+ [c, g.cx, g.cy],
174
+ [s, g.x1, g.y1],
175
+ [e, g.x2, g.y2],
176
+ ]) {
177
+ w.push_primitive({ type: 'point', id, x, y, fixed: false });
178
+ }
179
+ w.push_primitive({
180
+ type: 'arc', id: `A${g.index}`, c_id: c, start_id: s, end_id: e,
181
+ start_angle: g.startAngle, end_angle: g.endAngle, radius: g.radius,
182
+ });
183
+ arcs.set(g.index, { center: c, start: s, end: e });
184
+ break;
185
+ }
186
+ case 'ellipse': {
187
+ const c = P(g.index, 3);
188
+ const f1 = P(g.index, 1);
189
+ for (const [id, x, y] of [
190
+ [c, g.cx, g.cy],
191
+ [f1, g.fx1, g.fy1],
192
+ ]) {
193
+ w.push_primitive({ type: 'point', id, x, y, fixed: false });
194
+ }
195
+ w.push_primitive({
196
+ type: 'ellipse', id: `E${g.index}`, c_id: c, focus1_id: f1, radmin: g.minorRadius,
197
+ });
198
+ ellipses.set(g.index, { center: c, focus1: f1 });
199
+ break;
200
+ }
201
+ case 'bspline': {
202
+ // P4: the spline itself is not pushed to GCS (planegcs has no b-spline
203
+ // primitive); its exact endpoints participate as a line proxy so
204
+ // coincident/dimension constraints on the curve ends still solve.
205
+ // Interior shape is preserved at contour extraction (de Boor sampling).
206
+ const p1 = P(g.index, 1);
207
+ const p2 = P(g.index, 2);
208
+ for (const [id, x, y] of [
209
+ [p1, g.x1, g.y1],
210
+ [p2, g.x2, g.y2],
211
+ ]) {
212
+ w.push_primitive({ type: 'point', id, x, y, fixed: false });
213
+ }
214
+ w.push_primitive({ type: 'line', id: `L${g.index}`, p1_id: p1, p2_id: p2 });
215
+ lines.set(g.index, { p1, p2 });
216
+ break;
217
+ }
218
+ case 'point':
219
+ break; // handled in pass 1
220
+ }
221
+ }
222
+ // --- M3.4: constraints → primitives ---
223
+ // A constraint whose GCS shape cannot be pushed is dropped and recorded
224
+ // (D3: no silent loss — surfaced via droppedConstraints).
225
+ const droppedConstraints = [...unresolvableExternal, ...ignorable];
226
+ for (const c of usableConstraints) {
227
+ // P3-2: ignorable types (15/17/19) were recorded above; never pushed.
228
+ if (ignorableSet.has(c.index))
229
+ continue;
230
+ let prims;
231
+ try {
232
+ prims = this.constraintToPrimitives(c, { lines, arcs, circles, ellipses, standalone, externalLines });
233
+ }
234
+ catch {
235
+ droppedConstraints.push(c.index);
236
+ continue;
237
+ }
238
+ try {
239
+ for (const p of prims)
240
+ w.push_primitive(p);
241
+ }
242
+ catch {
243
+ droppedConstraints.push(c.index);
244
+ continue;
245
+ }
246
+ }
247
+ // --- solve (D2: initial values = on-disk coordinates) ---
248
+ const status = w.solve(Algorithm.LevenbergMarquardt);
249
+ const conflicting = w.has_gcs_conflicting_constraints();
250
+ const redundant = w.has_gcs_redundant_constraints();
251
+ if (conflicting) {
252
+ const problemConstraints = w.get_gcs_conflicting_constraints()
253
+ .map((s) => Number(s.replace(/^c/, '')))
254
+ .filter((n) => Number.isFinite(n));
255
+ return ok({
256
+ geoms,
257
+ converged: false,
258
+ reason: 'conflicting',
259
+ problemConstraints,
260
+ droppedConstraints,
261
+ });
262
+ }
263
+ if (redundant) {
264
+ // D3: redundant constraints are dropped internally by planegcs's
265
+ // redundant-solving mode; the best-effort solution is still valid.
266
+ w.apply_solution();
267
+ const solved = this.pullBack(geoms, { lines, arcs, circles, ellipses, standalone });
268
+ const problemConstraints = w.get_gcs_redundant_constraints()
269
+ .map((s) => Number(s.replace(/^c/, '')))
270
+ .filter((n) => Number.isFinite(n));
271
+ return ok({
272
+ geoms: solved,
273
+ converged: true,
274
+ reason: 'redundant',
275
+ problemConstraints,
276
+ droppedConstraints: [...droppedConstraints, ...problemConstraints],
277
+ });
278
+ }
279
+ if (status !== 0) {
280
+ return ok({
281
+ geoms,
282
+ converged: false,
283
+ reason: 'failed',
284
+ problemConstraints: [],
285
+ droppedConstraints,
286
+ });
287
+ }
288
+ w.apply_solution();
289
+ // --- pull back solved coordinates and rebuild geometry ---
290
+ const solved = this.pullBack(geoms, { lines, arcs, circles, ellipses, standalone });
291
+ return ok({ geoms: solved, converged: true, problemConstraints: [], droppedConstraints });
292
+ }
293
+ /**
294
+ * Remaining degrees of freedom after a successful solve (-1 if unavailable).
295
+ *
296
+ * @returns the DoF count, or -1 if the backend does not report it.
297
+ */
298
+ getDof() {
299
+ try {
300
+ const v = this.wrapper.gcs.dof();
301
+ return typeof v === 'number' && Number.isFinite(v) ? v : -1;
302
+ }
303
+ catch {
304
+ return -1;
305
+ }
306
+ }
307
+ constraintToPrimitives(c, ctx) {
308
+ const out = [];
309
+ const id = `c${c.index}`;
310
+ const r = c.refs;
311
+ // guards: old-format fallback may yield < 2 refs for pair constraints
312
+ if (r.length < 1)
313
+ return out;
314
+ const pt = (ref) => {
315
+ if (!ref)
316
+ return undefined;
317
+ if (ref.geoId === -1)
318
+ return P(-1, 1); // root point
319
+ if (ref.geoId === -2)
320
+ return P(-2, ref.pos === 0 ? 1 : ref.pos); // VAxis point
321
+ if (ref.geoId <= -3 && ref.geoId > -2000) {
322
+ // M6.3 external geometry: endpoint/point refs onto fixed segments
323
+ const ext = ctx.externalLines.get(ref.geoId);
324
+ if (!ext)
325
+ return undefined;
326
+ if (ref.pos === 1)
327
+ return ext.p1;
328
+ if (ref.pos === 2)
329
+ return ext.p2;
330
+ return undefined; // pos=0 (edge itself) handled by line-typed paths
331
+ }
332
+ if (ref.geoId >= 0)
333
+ return P(ref.geoId, ref.pos);
334
+ return undefined;
335
+ };
336
+ switch (c.type) {
337
+ case ConstraintType.Coincident: {
338
+ const p1 = pt(r[0]);
339
+ const p2 = pt(r[1]);
340
+ if (p1 && p2)
341
+ out.push({ type: 'p2p_coincident', id, p1_id: p1, p2_id: p2 });
342
+ break;
343
+ }
344
+ case ConstraintType.Horizontal: {
345
+ // Horizontal with pos=0 refs is the HAxis case: line horizontal
346
+ if (r[0].pos === PointPos.none || r.length >= 1 && r[0].pos !== PointPos.mid) {
347
+ if (r.length === 1) {
348
+ // horizontal via axis: geoId is the line itself
349
+ const line = ctx.lines.get(r[0].geoId);
350
+ if (line)
351
+ out.push({ type: 'horizontal_l', id, l_id: line ? `L${r[0].geoId}` : '' });
352
+ }
353
+ else {
354
+ const p1 = pt(r[0]);
355
+ const p2 = pt(r[1]);
356
+ if (p1 && p2)
357
+ out.push({ type: 'horizontal_pp', id, p1_id: p1, p2_id: p2 });
358
+ }
359
+ }
360
+ break;
361
+ }
362
+ case ConstraintType.Vertical: {
363
+ if (r.length === 1) {
364
+ const gid = `L${r[0].geoId}`;
365
+ if (ctx.lines.has(r[0].geoId))
366
+ out.push({ type: 'vertical_l', id, l_id: gid });
367
+ }
368
+ else {
369
+ const p1 = pt(r[0]);
370
+ const p2 = pt(r[1]);
371
+ if (p1 && p2)
372
+ out.push({ type: 'vertical_pp', id, p1_id: p1, p2_id: p2 });
373
+ }
374
+ break;
375
+ }
376
+ case ConstraintType.Parallel: {
377
+ if (r.length === 2 && ctx.lines.has(r[0].geoId) && ctx.lines.has(r[1].geoId)) {
378
+ out.push({
379
+ type: 'parallel', id,
380
+ l1_id: `L${r[0].geoId}`, l2_id: `L${r[1].geoId}`,
381
+ internalalignment: 0,
382
+ });
383
+ }
384
+ break;
385
+ }
386
+ case ConstraintType.Perpendicular: {
387
+ if (r.length === 2 && ctx.lines.has(r[0].geoId) && ctx.lines.has(r[1].geoId)) {
388
+ out.push({ type: 'perpendicular_ll', id, l1_id: `L${r[0].geoId}`, l2_id: `L${r[1].geoId}` });
389
+ }
390
+ break;
391
+ }
392
+ case ConstraintType.Distance: {
393
+ // p2p / p2l / p2c / c2c variants by ref shapes.
394
+ // GOTCHA (probe-l1-ablation.ts): a second ref that is an AXIS EDGE
395
+ // (geoId -1/-2, pos=0) is a point-to-LINE distance against the axis
396
+ // (FreeCAD Sketch.cpp addDistanceConstraint(point, line)) — e.g.
397
+ // Distance(p, HAxis) = |y|. Resolving the axis edge to the root point
398
+ // and emitting p2p_distance instead drags the point onto a circle
399
+ // around the origin (taperedballnose collapse, delta 2.3e1).
400
+ const p1 = pt(r[0]);
401
+ const axisEdgeDist = r.length >= 2 && r[1].pos === PointPos.none && (r[1].geoId === -1 || r[1].geoId === -2);
402
+ if (axisEdgeDist && p1) {
403
+ out.push({ type: 'p2l_distance', id, p_id: p1, l_id: `L${r[1].geoId}`, distance: c.value });
404
+ }
405
+ else if (p1 && r[1] && r[1].pos === PointPos.none && ctx.lines.has(r[1].geoId)) {
406
+ out.push({ type: 'p2l_distance', id, p_id: p1, l_id: `L${r[1].geoId}`, distance: c.value });
407
+ }
408
+ else if (p1 && r[1]) {
409
+ const p2 = pt(r[1]);
410
+ if (p2)
411
+ out.push({ type: 'p2p_distance', id, p1_id: p1, p2_id: p2, distance: c.value });
412
+ }
413
+ break;
414
+ }
415
+ case ConstraintType.DistanceX: {
416
+ // planegcs 'difference' semantics: param2 - param1 = difference
417
+ // (verified empirically). FCStd DistanceX value = second.x - first.x.
418
+ // GOTCHA (probe-l1-ablation.ts, Sketch.cpp:2053): when the second ref
419
+ // is an AXIS EDGE (geoId -1/-2, pos=0) FreeCAD treats it as GeoUndef —
420
+ // an ABSOLUTE coordinate constraint (coordinate_x), not a difference
421
+ // against the axis point. Mapping it to 'difference' forces x = -value
422
+ // and produced the taperedballnose wrong solution (delta 2.3e1).
423
+ const axisEdge = r.length >= 2 && r[1].pos === PointPos.none && (r[1].geoId === -1 || r[1].geoId === -2);
424
+ const firstIsPoint = r[0].pos !== PointPos.none || r[0].geoId < 0;
425
+ if (axisEdge && firstIsPoint) {
426
+ const p = pt(r[0]);
427
+ if (p)
428
+ out.push({ type: 'coordinate_x', id, p_id: p, x: c.value });
429
+ break;
430
+ }
431
+ const p1 = pt(r[0]);
432
+ const p2 = pt(r[1]);
433
+ if (p1 && p2) {
434
+ out.push({
435
+ type: 'difference',
436
+ id,
437
+ param1: { o_id: p1, prop: 'x' },
438
+ param2: { o_id: p2, prop: 'x' },
439
+ difference: c.value,
440
+ });
441
+ }
442
+ break;
443
+ }
444
+ case ConstraintType.DistanceY: {
445
+ // symmetric case to DistanceX: axis-edge second ref → absolute y
446
+ const axisEdge = r.length >= 2 && r[1].pos === PointPos.none && (r[1].geoId === -1 || r[1].geoId === -2);
447
+ const firstIsPoint = r[0].pos !== PointPos.none || r[0].geoId < 0;
448
+ if (axisEdge && firstIsPoint) {
449
+ const p = pt(r[0]);
450
+ if (p)
451
+ out.push({ type: 'coordinate_y', id, p_id: p, y: c.value });
452
+ break;
453
+ }
454
+ const p1 = pt(r[0]);
455
+ const p2 = pt(r[1]);
456
+ if (p1 && p2) {
457
+ out.push({
458
+ type: 'difference',
459
+ id,
460
+ param1: { o_id: p1, prop: 'y' },
461
+ param2: { o_id: p2, prop: 'y' },
462
+ difference: c.value,
463
+ });
464
+ }
465
+ break;
466
+ }
467
+ case ConstraintType.Angle: {
468
+ // FreeCAD Sketch.cpp case Angle — 4 shapes by ref count/pos:
469
+ // ① Third != GeoUndef → angle-via-point (not needed by corpus P0 set;
470
+ // dropped explicitly below if encountered)
471
+ // ② SecondPos != none → l2l_angle_pppp (lines with explicit start points:
472
+ // pos==start keeps direction, pos==end swaps the point pair)
473
+ // ③ Second set, both pos none → l2l_angle_ll (line-level)
474
+ // ④ only First → p2p_angle on the line's own points (orientation)
475
+ // refs may carry a Third = -2000 (GeoUndef sentinel) — filter it.
476
+ const refs = r.filter((x) => x.geoId !== -2000);
477
+ if (refs.length >= 3) {
478
+ // ① via-point angle: not in the P0 mapping set — record as dropped
479
+ break;
480
+ }
481
+ if (refs.length === 2) {
482
+ const [a, b] = refs;
483
+ const lineA = ctx.lines.has(a.geoId);
484
+ const lineB = ctx.lines.has(b.geoId);
485
+ if (lineA && lineB && a.pos !== PointPos.none && b.pos !== PointPos.none) {
486
+ // ② explicit start points; pos=end reverses that line's direction
487
+ const a1 = a.pos === PointPos.start ? P(a.geoId, 1) : P(a.geoId, 2);
488
+ const a2 = a.pos === PointPos.start ? P(a.geoId, 2) : P(a.geoId, 1);
489
+ const b1 = b.pos === PointPos.start ? P(b.geoId, 1) : P(b.geoId, 2);
490
+ const b2 = b.pos === PointPos.start ? P(b.geoId, 2) : P(b.geoId, 1);
491
+ out.push({ type: 'l2l_angle_pppp', id, l1p1_id: a1, l1p2_id: a2, l2p1_id: b1, l2p2_id: b2, angle: c.value });
492
+ }
493
+ else if (lineA && lineB) {
494
+ // ③ line-level angle
495
+ out.push({
496
+ type: 'l2l_angle_ll', id,
497
+ l1_id: `L${a.geoId}`, l2_id: `L${b.geoId}`,
498
+ angle: c.value,
499
+ internalalignment: 0,
500
+ });
501
+ }
502
+ }
503
+ else if (refs.length === 1 && ctx.lines.has(refs[0].geoId)) {
504
+ // ④ orientation angle of a single line (p2p_angle on its points)
505
+ out.push({ type: 'p2p_angle', id, p1_id: P(refs[0].geoId, 1), p2_id: P(refs[0].geoId, 2), angle: c.value });
506
+ }
507
+ break;
508
+ }
509
+ case ConstraintType.Radius: {
510
+ if (ctx.circles.has(r[0].geoId)) {
511
+ out.push({ type: 'circle_radius', id, c_id: `C${r[0].geoId}`, radius: c.value });
512
+ }
513
+ else if (ctx.arcs.has(r[0].geoId)) {
514
+ out.push({ type: 'arc_radius', id, a_id: `A${r[0].geoId}`, radius: c.value });
515
+ }
516
+ break;
517
+ }
518
+ case ConstraintType.Diameter: {
519
+ if (ctx.circles.has(r[0].geoId)) {
520
+ out.push({ type: 'circle_diameter', id, c_id: `C${r[0].geoId}`, diameter: c.value });
521
+ }
522
+ else if (ctx.arcs.has(r[0].geoId)) {
523
+ out.push({ type: 'arc_diameter', id, a_id: `A${r[0].geoId}`, diameter: c.value });
524
+ }
525
+ break;
526
+ }
527
+ case ConstraintType.Equal: {
528
+ // equal_length for line pairs; equal_radii for circle/arc pairs
529
+ if (r.length === 2) {
530
+ const g0 = r[0].geoId;
531
+ const g1 = r[1].geoId;
532
+ if (ctx.lines.has(g0) && ctx.lines.has(g1)) {
533
+ out.push({ type: 'equal_length', id, l1_id: `L${g0}`, l2_id: `L${g1}` });
534
+ }
535
+ else if ((ctx.circles.has(g0) || ctx.arcs.has(g0)) && (ctx.circles.has(g1) || ctx.arcs.has(g1))) {
536
+ // arcs use their own primitive id (A{n}); circles use C{n}
537
+ const id0 = ctx.circles.has(g0) ? `C${g0}` : `A${g0}`;
538
+ const id1 = ctx.circles.has(g1) ? `C${g1}` : `A${g1}`;
539
+ out.push({ type: 'equal_radius_cc', id, c1_id: id0, c2_id: id1 });
540
+ }
541
+ else if (ctx.ellipses.has(g0) && ctx.ellipses.has(g1)) {
542
+ out.push({ type: 'equal_radii_ee', id, e1_id: `E${g0}`, e2_id: `E${g1}` });
543
+ }
544
+ }
545
+ break;
546
+ }
547
+ case ConstraintType.PointOnObject: {
548
+ const p = pt(r[0]);
549
+ if (p && r[1].pos === PointPos.none) {
550
+ const onGeoId = r[1].geoId;
551
+ if (ctx.lines.has(onGeoId)) {
552
+ out.push({ type: 'point_on_line_pl', id, p_id: p, l_id: `L${onGeoId}` });
553
+ }
554
+ else if (ctx.externalLines.has(onGeoId)) {
555
+ // M6.3: point-on-external-edge → point_on_line against the fixed line
556
+ out.push({ type: 'point_on_line_pl', id, p_id: p, l_id: `X${onGeoId}` });
557
+ }
558
+ else if (ctx.circles.has(onGeoId)) {
559
+ out.push({ type: 'point_on_circle', id, p_id: p, c_id: `C${onGeoId}` });
560
+ }
561
+ else if (ctx.arcs.has(onGeoId)) {
562
+ out.push({ type: 'point_on_arc', id, p_id: p, a_id: `A${onGeoId}` });
563
+ }
564
+ }
565
+ break;
566
+ }
567
+ case ConstraintType.Symmetric: {
568
+ // Symmetric(p1, p2, line) or Symmetric(p1, p2, p3)
569
+ const p1 = pt(r[0]);
570
+ const p2 = pt(r[1]);
571
+ if (p1 && p2) {
572
+ if (r.length >= 3 && r[2].pos === PointPos.none && ctx.lines.has(r[2].geoId)) {
573
+ out.push({ type: 'p2p_symmetric_ppl', id, p1_id: p1, p2_id: p2, l_id: `L${r[2].geoId}` });
574
+ }
575
+ else {
576
+ const p3 = pt(r[2]);
577
+ if (p3)
578
+ out.push({ type: 'p2p_symmetric_ppp', id, p1_id: p1, p2_id: p2, p_id: p3 });
579
+ }
580
+ }
581
+ break;
582
+ }
583
+ case ConstraintType.Tangent: {
584
+ // line-circle / circle-circle / line-arc / arc-arc (edge-level, pos=0)
585
+ const a = r[0];
586
+ const b = r[1];
587
+ if (!a || !b)
588
+ break;
589
+ const lineA = ctx.lines.has(a.geoId);
590
+ const circA = ctx.circles.has(a.geoId);
591
+ const arcA = ctx.arcs.has(a.geoId);
592
+ const lineB = ctx.lines.has(b.geoId);
593
+ const circB = ctx.circles.has(b.geoId);
594
+ const arcB = ctx.arcs.has(b.geoId);
595
+ if (lineA && circB)
596
+ out.push({ type: 'tangent_lc', id, l_id: `L${a.geoId}`, c_id: `C${b.geoId}`, internalalignment: 0 });
597
+ else if (circA && lineB)
598
+ out.push({ type: 'tangent_lc', id, l_id: `L${b.geoId}`, c_id: `C${a.geoId}`, internalalignment: 0 });
599
+ else if (circA && circB)
600
+ out.push({ type: 'tangent_cc', id, c1_id: `C${a.geoId}`, c2_id: `C${b.geoId}`, internalalignment: 0 });
601
+ else if (lineA && arcB)
602
+ out.push({ type: 'tangent_la', id, l_id: `L${a.geoId}`, a_id: `A${b.geoId}`, internalalignment: 0 });
603
+ else if (arcA && lineB)
604
+ out.push({ type: 'tangent_la', id, l_id: `L${b.geoId}`, a_id: `A${a.geoId}`, internalalignment: 0 });
605
+ else if (arcA && arcB)
606
+ out.push({ type: 'tangent_aa', id, a1_id: `A${a.geoId}`, a2_id: `A${b.geoId}`, internalalignment: 0 });
607
+ break;
608
+ }
609
+ default:
610
+ break; // unsupported types never reach here (guard in solve())
611
+ }
612
+ return out;
613
+ }
614
+ pullBack(geoms, ctx) {
615
+ // After apply_solution, re-derive geometry from the wrapper's primitive
616
+ // state via pull through the wrapper's sketch index.
617
+ const out = [];
618
+ for (const g of geoms) {
619
+ switch (g.kind) {
620
+ case 'point': {
621
+ const key = ctx.standalone.get(g.index);
622
+ const pt = key ? this.readPoint(key) : undefined;
623
+ out.push(pt ? { ...g, x: pt.x, y: pt.y } : g);
624
+ break;
625
+ }
626
+ case 'line': {
627
+ const l = ctx.lines.get(g.index);
628
+ const p1 = this.readPoint(l.p1);
629
+ const p2 = this.readPoint(l.p2);
630
+ out.push(p1 && p2 ? { ...g, x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y } : g);
631
+ break;
632
+ }
633
+ case 'circle': {
634
+ const cc = ctx.circles.get(g.index).center;
635
+ const c = this.readPoint(cc);
636
+ const rad = this.readCircleRadius(g.index);
637
+ out.push(c && rad !== undefined ? { ...g, cx: c.x, cy: c.y, radius: rad } : g);
638
+ break;
639
+ }
640
+ case 'arc': {
641
+ const a = ctx.arcs.get(g.index);
642
+ const c = this.readPoint(a.center);
643
+ const s = this.readPoint(a.start);
644
+ const e = this.readPoint(a.end);
645
+ if (c && s && e) {
646
+ const r = Math.hypot(s.x - c.x, s.y - c.y);
647
+ const startAngle = Math.atan2(s.y - c.y, s.x - c.x);
648
+ const endAngle = Math.atan2(e.y - c.y, e.x - c.x);
649
+ out.push({ ...g, cx: c.x, cy: c.y, radius: r, startAngle, endAngle, x1: s.x, y1: s.y, x2: e.x, y2: e.y });
650
+ }
651
+ else
652
+ out.push(g);
653
+ break;
654
+ }
655
+ case 'bspline': {
656
+ // P4: spline endpoints ride the line proxy; pull their solved coords.
657
+ const l = ctx.lines.get(g.index);
658
+ const p1 = this.readPoint(l.p1);
659
+ const p2 = this.readPoint(l.p2);
660
+ out.push(p1 && p2 ? { ...g, x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y } : g);
661
+ break;
662
+ }
663
+ case 'ellipse': {
664
+ const el = ctx.ellipses.get(g.index);
665
+ const c = this.readPoint(el.center);
666
+ const f1 = this.readPoint(el.focus1);
667
+ if (c && f1) {
668
+ const major = Math.hypot(f1.x - c.x, f1.y - c.y) * 1;
669
+ const angleXU = Math.atan2(f1.y - c.y, f1.x - c.x);
670
+ // minor radius is a parameter on the ellipse primitive; approximate
671
+ // pull via ratio is not exposed — keep stored minor (M3 scope).
672
+ out.push({ ...g, cx: c.x, cy: c.y, majorRadius: g.majorRadius * (major / g.majorRadius), angleXU });
673
+ }
674
+ else
675
+ out.push(g);
676
+ break;
677
+ }
678
+ }
679
+ }
680
+ return out;
681
+ }
682
+ /** Read a point's solved coordinates from the sketch index.
683
+ * apply_solution() already pulled every primitive's solved values back into
684
+ * sketch_index (gcs_wrapper.js:104-108), so get_primitive is authoritative. */
685
+ readPoint(key) {
686
+ const p = this.wrapper.sketch_index.get_primitive(key);
687
+ if (!p || typeof p.x !== 'number' || typeof p.y !== 'number')
688
+ return undefined;
689
+ return { x: p.x, y: p.y };
690
+ }
691
+ readCircleRadius(geoIndex) {
692
+ const c = this.wrapper.sketch_index.get_primitive(`C${geoIndex}`);
693
+ return typeof c?.radius === 'number' ? c.radius : undefined;
694
+ }
695
+ }
696
+ //# sourceMappingURL=planegcs-backend.js.map