fluidcad 0.0.30 → 0.0.32

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 (135) hide show
  1. package/lib/dist/common/build-error.d.ts +13 -0
  2. package/lib/dist/common/build-error.js +18 -0
  3. package/lib/dist/common/describe-error.d.ts +6 -0
  4. package/lib/dist/common/describe-error.js +26 -0
  5. package/lib/dist/common/operand-check.d.ts +19 -0
  6. package/lib/dist/common/operand-check.js +38 -0
  7. package/lib/dist/common/scene-object.d.ts +8 -0
  8. package/lib/dist/common/scene-object.js +10 -0
  9. package/lib/dist/common/shape-factory.d.ts +1 -1
  10. package/lib/dist/core/2d/arc.d.ts +4 -2
  11. package/lib/dist/core/2d/hmove.d.ts +8 -1
  12. package/lib/dist/core/2d/hmove.js +6 -2
  13. package/lib/dist/core/2d/pmove.d.ts +13 -3
  14. package/lib/dist/core/2d/pmove.js +6 -2
  15. package/lib/dist/core/2d/vmove.d.ts +8 -1
  16. package/lib/dist/core/2d/vmove.js +8 -4
  17. package/lib/dist/core/extrude.d.ts +17 -4
  18. package/lib/dist/core/extrude.js +8 -6
  19. package/lib/dist/core/interfaces.d.ts +16 -6
  20. package/lib/dist/core/mirror.d.ts +5 -5
  21. package/lib/dist/features/2d/aline.js +6 -2
  22. package/lib/dist/features/2d/arc.d.ts +3 -0
  23. package/lib/dist/features/2d/arc.js +28 -1
  24. package/lib/dist/features/2d/hline.js +5 -1
  25. package/lib/dist/features/2d/hmove.d.ts +2 -2
  26. package/lib/dist/features/2d/hmove.js +32 -7
  27. package/lib/dist/features/2d/intersect.js +17 -10
  28. package/lib/dist/features/2d/line.d.ts +2 -0
  29. package/lib/dist/features/2d/line.js +4 -0
  30. package/lib/dist/features/2d/pmove.d.ts +2 -2
  31. package/lib/dist/features/2d/pmove.js +47 -7
  32. package/lib/dist/features/2d/projection.d.ts +1 -1
  33. package/lib/dist/features/2d/projection.js +25 -15
  34. package/lib/dist/features/2d/sketch.d.ts +2 -2
  35. package/lib/dist/features/2d/sketch.js +10 -4
  36. package/lib/dist/features/2d/tarc-to-point.js +0 -3
  37. package/lib/dist/features/2d/tarc.js +0 -3
  38. package/lib/dist/features/2d/tline.js +0 -3
  39. package/lib/dist/features/2d/vline.js +5 -1
  40. package/lib/dist/features/2d/vmove.d.ts +2 -2
  41. package/lib/dist/features/2d/vmove.js +32 -7
  42. package/lib/dist/features/axis-from-edge.d.ts +1 -0
  43. package/lib/dist/features/axis-from-edge.js +8 -0
  44. package/lib/dist/features/chamfer.d.ts +1 -0
  45. package/lib/dist/features/chamfer.js +6 -0
  46. package/lib/dist/features/color.d.ts +1 -0
  47. package/lib/dist/features/color.js +6 -0
  48. package/lib/dist/features/common.d.ts +1 -0
  49. package/lib/dist/features/common.js +9 -0
  50. package/lib/dist/features/common2d.d.ts +1 -0
  51. package/lib/dist/features/common2d.js +9 -0
  52. package/lib/dist/features/draft.d.ts +1 -0
  53. package/lib/dist/features/draft.js +6 -0
  54. package/lib/dist/features/extrude-to-face.d.ts +5 -1
  55. package/lib/dist/features/extrude-to-face.js +50 -8
  56. package/lib/dist/features/extrude.js +19 -28
  57. package/lib/dist/features/fillet.d.ts +1 -0
  58. package/lib/dist/features/fillet.js +6 -0
  59. package/lib/dist/features/fillet2d.d.ts +1 -0
  60. package/lib/dist/features/fillet2d.js +9 -0
  61. package/lib/dist/features/fuse.d.ts +1 -0
  62. package/lib/dist/features/fuse.js +6 -0
  63. package/lib/dist/features/fuse2d.d.ts +1 -0
  64. package/lib/dist/features/fuse2d.js +9 -0
  65. package/lib/dist/features/loft.d.ts +1 -0
  66. package/lib/dist/features/loft.js +6 -0
  67. package/lib/dist/features/mirror-shape.d.ts +1 -0
  68. package/lib/dist/features/mirror-shape.js +28 -8
  69. package/lib/dist/features/plane-from-object.d.ts +1 -0
  70. package/lib/dist/features/plane-from-object.js +8 -0
  71. package/lib/dist/features/rotate.d.ts +1 -0
  72. package/lib/dist/features/rotate.js +11 -2
  73. package/lib/dist/features/select.d.ts +1 -0
  74. package/lib/dist/features/select.js +40 -12
  75. package/lib/dist/features/shell.d.ts +1 -0
  76. package/lib/dist/features/shell.js +6 -0
  77. package/lib/dist/features/simple-extruder.js +6 -3
  78. package/lib/dist/features/subtract.d.ts +1 -0
  79. package/lib/dist/features/subtract.js +5 -0
  80. package/lib/dist/features/subtract2d.d.ts +1 -0
  81. package/lib/dist/features/subtract2d.js +5 -0
  82. package/lib/dist/features/sweep.d.ts +1 -0
  83. package/lib/dist/features/sweep.js +4 -0
  84. package/lib/dist/features/translate.d.ts +1 -0
  85. package/lib/dist/features/translate.js +9 -0
  86. package/lib/dist/filters/face/above-below.d.ts +20 -0
  87. package/lib/dist/filters/face/above-below.js +57 -0
  88. package/lib/dist/filters/face/face-filter.d.ts +26 -0
  89. package/lib/dist/filters/face/face-filter.js +64 -0
  90. package/lib/dist/filters/face/planar-filter.d.ts +15 -0
  91. package/lib/dist/filters/face/planar-filter.js +30 -0
  92. package/lib/dist/filters/from-object.d.ts +1 -0
  93. package/lib/dist/filters/from-object.js +3 -0
  94. package/lib/dist/oc/boolean-ops.d.ts +2 -2
  95. package/lib/dist/oc/boolean-ops.js +8 -3
  96. package/lib/dist/oc/edge-ops.d.ts +17 -0
  97. package/lib/dist/oc/edge-ops.js +60 -0
  98. package/lib/dist/oc/face-maker2.d.ts +8 -0
  99. package/lib/dist/oc/face-maker2.js +42 -1
  100. package/lib/dist/oc/face-ops.d.ts +6 -1
  101. package/lib/dist/oc/face-ops.js +3 -2
  102. package/lib/dist/oc/face-query.js +19 -15
  103. package/lib/dist/oc/ray-intersect.d.ts +3 -2
  104. package/lib/dist/oc/ray-intersect.js +2 -4
  105. package/lib/dist/oc/shell-ops.js +15 -2
  106. package/lib/dist/oc/thin-face-maker.d.ts +15 -0
  107. package/lib/dist/oc/thin-face-maker.js +48 -7
  108. package/lib/dist/oc/wire-ops.d.ts +14 -0
  109. package/lib/dist/oc/wire-ops.js +38 -0
  110. package/lib/dist/rendering/render.js +6 -4
  111. package/lib/dist/tests/common/describe-error.test.d.ts +1 -0
  112. package/lib/dist/tests/common/describe-error.test.js +36 -0
  113. package/lib/dist/tests/features/2d/intersect.test.js +43 -0
  114. package/lib/dist/tests/features/2d/move.test.js +72 -1
  115. package/lib/dist/tests/features/2d/project-regression.test.js +35 -0
  116. package/lib/dist/tests/features/color-lineage.test.js +24 -0
  117. package/lib/dist/tests/features/cut.test.js +40 -0
  118. package/lib/dist/tests/features/cylinder-curve-filter.test.d.ts +1 -0
  119. package/lib/dist/tests/features/cylinder-curve-filter.test.js +99 -0
  120. package/lib/dist/tests/features/extrude-to-face.test.js +52 -0
  121. package/lib/dist/tests/features/extrude.test.js +46 -8
  122. package/lib/dist/tests/features/mirror.test.js +74 -0
  123. package/lib/dist/tests/features/select.test.js +141 -0
  124. package/lib/dist/tests/features/subtract-consumed-input.test.d.ts +1 -0
  125. package/lib/dist/tests/features/subtract-consumed-input.test.js +28 -0
  126. package/lib/dist/tests/features/thin-extrude-offset-fix.test.d.ts +1 -0
  127. package/lib/dist/tests/features/thin-extrude-offset-fix.test.js +34 -0
  128. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  129. package/package.json +2 -3
  130. package/ui/dist/assets/{index-6Ep4GPxf.js → index-DMw0OYCF.js} +70 -70
  131. package/ui/dist/assets/index-DR7c2Qk9.css +2 -0
  132. package/ui/dist/index.html +2 -2
  133. package/lib/dist/features/infinite-extrude.d.ts +0 -13
  134. package/lib/dist/features/infinite-extrude.js +0 -79
  135. package/ui/dist/assets/index-DRKfe6N9.css +0 -2
@@ -1,5 +1,6 @@
1
1
  import { Vertex } from "../../common/vertex.js";
2
2
  import { SectionOps } from "../../oc/section-ops.js";
3
+ import { WireOps } from "../../oc/wire-ops.js";
3
4
  import { ExtrudableGeometryBase } from "./extrudable-base.js";
4
5
  export class Intersect extends ExtrudableGeometryBase {
5
6
  sourceObjects;
@@ -11,19 +12,25 @@ export class Intersect extends ExtrudableGeometryBase {
11
12
  const plane = this.targetPlane?.getPlane() || this.sketch.getPlane();
12
13
  const shapes = this.sourceObjects.flatMap(obj => obj.getShapes());
13
14
  const transform = context?.getTransform() ?? null;
14
- let lastEdge = null;
15
- for (let shape of shapes) {
15
+ const allEdges = [];
16
+ for (const shape of shapes) {
16
17
  const edges = SectionOps.sectionShapeWithPlane(plane, shape);
17
- for (const edge of edges) {
18
- lastEdge = edge;
19
- }
18
+ allEdges.push(...edges);
20
19
  this.addShapes(edges);
21
20
  }
22
- if (lastEdge) {
23
- const localStart = plane.worldToLocal(lastEdge.getFirstVertex().toPoint());
24
- const localEnd = plane.worldToLocal(lastEdge.getLastVertex().toPoint());
25
- this.setState('start', Vertex.fromPoint2D(localStart));
26
- this.setState('end', Vertex.fromPoint2D(localEnd));
21
+ // Section across multiple source faces yields an unordered edge set that
22
+ // may form one connected chain, several disjoint chains, or closed loops.
23
+ // Take the first connected group and use its actual chain endpoints —
24
+ // not an arbitrary edge's vertices, which can land on interior junctions.
25
+ if (allEdges.length > 0) {
26
+ const groups = WireOps.groupConnectedEdges(allEdges);
27
+ const endpoints = WireOps.findChainEndpoints(groups[0]);
28
+ if (endpoints) {
29
+ const localStart = plane.worldToLocal(endpoints.start.toPoint());
30
+ const localEnd = plane.worldToLocal(endpoints.end.toPoint());
31
+ this.setState('start', Vertex.fromPoint2D(localStart));
32
+ this.setState('end', Vertex.fromPoint2D(localEnd));
33
+ }
27
34
  }
28
35
  for (const obj of this.sourceObjects) {
29
36
  obj.removeShapes(this);
@@ -1,3 +1,4 @@
1
+ import { SceneObject } from "../../common/scene-object.js";
1
2
  import { LazyVertex } from "../lazy-vertex.js";
2
3
  import { PlaneObjectBase } from "../plane-renderable-base.js";
3
4
  import { GeometrySceneObject } from "./geometry.js";
@@ -6,6 +7,7 @@ export declare class LineTo extends GeometrySceneObject {
6
7
  private targetPlane;
7
8
  constructor(endPoint: LazyVertex, targetPlane?: PlaneObjectBase);
8
9
  build(): void;
10
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
9
11
  compareTo(other: LineTo): boolean;
10
12
  getType(): string;
11
13
  getUniqueType(): string;
@@ -30,6 +30,10 @@ export class LineTo extends GeometrySceneObject {
30
30
  this.targetPlane.removeShapes(this);
31
31
  }
32
32
  }
33
+ createCopy(remap) {
34
+ const targetPlane = this.targetPlane ? (remap.get(this.targetPlane) || this.targetPlane) : null;
35
+ return new LineTo(this.endPoint, targetPlane);
36
+ }
33
37
  compareTo(other) {
34
38
  if (!(other instanceof LineTo)) {
35
39
  return false;
@@ -1,9 +1,9 @@
1
1
  import { SceneObject } from "../../common/scene-object.js";
2
2
  import { GeometrySceneObject } from "./geometry.js";
3
3
  export declare class PolarMove extends GeometrySceneObject {
4
- radius: number;
4
+ radiusOrTarget: number | SceneObject;
5
5
  angle: number;
6
- constructor(radius: number, angle: number);
6
+ constructor(radiusOrTarget: number | SceneObject, angle: number);
7
7
  getType(): string;
8
8
  build(): void;
9
9
  getDependencies(): SceneObject[];
@@ -1,11 +1,13 @@
1
1
  import { Point2D } from "../../math/point.js";
2
+ import { SceneObject } from "../../common/scene-object.js";
2
3
  import { GeometrySceneObject } from "./geometry.js";
4
+ import { findNearestRayIntersection } from "../../oc/ray-intersect.js";
3
5
  export class PolarMove extends GeometrySceneObject {
4
- radius;
6
+ radiusOrTarget;
5
7
  angle;
6
- constructor(radius, angle) {
8
+ constructor(radiusOrTarget, angle) {
7
9
  super();
8
- this.radius = radius;
10
+ this.radiusOrTarget = radiusOrTarget;
9
11
  this.angle = angle;
10
12
  }
11
13
  getType() {
@@ -13,14 +15,43 @@ export class PolarMove extends GeometrySceneObject {
13
15
  }
14
16
  build() {
15
17
  const pos = this.getCurrentPosition();
16
- const newPos = new Point2D(pos.x + this.radius * Math.cos(this.angle), pos.y + this.radius * Math.sin(this.angle));
18
+ const tangent = (this.sketch?.getTangentAt(this) ?? new Point2D(1, 0)).normalize();
19
+ const cos = Math.cos(this.angle);
20
+ const sin = Math.sin(this.angle);
21
+ const direction = new Point2D(cos * tangent.x - sin * tangent.y, sin * tangent.x + cos * tangent.y);
22
+ let newPos;
23
+ if (typeof this.radiusOrTarget === 'number') {
24
+ newPos = new Point2D(pos.x + this.radiusOrTarget * direction.x, pos.y + this.radiusOrTarget * direction.y);
25
+ }
26
+ else {
27
+ const plane = this.sketch.getPlane();
28
+ const hit = findNearestRayIntersection(plane, pos, direction, this.radiusOrTarget);
29
+ if (!hit) {
30
+ throw new Error("Cannot move at the specified angle up to the geometry");
31
+ }
32
+ newPos = hit;
33
+ }
17
34
  this.setCurrentPosition(newPos);
35
+ const moveVec = newPos.subtract(pos);
36
+ const moveLen = Math.hypot(moveVec.x, moveVec.y);
37
+ if (moveLen > 1e-12) {
38
+ this.setTangent(new Point2D(moveVec.x / moveLen, moveVec.y / moveLen));
39
+ }
40
+ else {
41
+ this.setTangent(direction);
42
+ }
18
43
  }
19
44
  getDependencies() {
45
+ if (this.radiusOrTarget instanceof SceneObject) {
46
+ return [this.radiusOrTarget];
47
+ }
20
48
  return [];
21
49
  }
22
50
  createCopy(remap) {
23
- return new PolarMove(this.radius, this.angle);
51
+ const radiusOrTarget = this.radiusOrTarget instanceof SceneObject
52
+ ? (remap.get(this.radiusOrTarget) || this.radiusOrTarget)
53
+ : this.radiusOrTarget;
54
+ return new PolarMove(radiusOrTarget, this.angle);
24
55
  }
25
56
  compareTo(other) {
26
57
  if (!(other instanceof PolarMove)) {
@@ -29,11 +60,20 @@ export class PolarMove extends GeometrySceneObject {
29
60
  if (!super.compareTo(other)) {
30
61
  return false;
31
62
  }
32
- return this.radius === other.radius && this.angle === other.angle;
63
+ if (this.angle !== other.angle) {
64
+ return false;
65
+ }
66
+ if (typeof this.radiusOrTarget !== typeof other.radiusOrTarget) {
67
+ return false;
68
+ }
69
+ if (this.radiusOrTarget instanceof SceneObject && other.radiusOrTarget instanceof SceneObject) {
70
+ return this.radiusOrTarget.compareTo(other.radiusOrTarget);
71
+ }
72
+ return this.radiusOrTarget === other.radiusOrTarget;
33
73
  }
34
74
  serialize() {
35
75
  return {
36
- radius: this.radius,
76
+ radius: typeof this.radiusOrTarget === 'number' ? this.radiusOrTarget : null,
37
77
  angle: this.angle
38
78
  };
39
79
  }
@@ -4,7 +4,7 @@ import { ExtrudableGeometryBase } from "./extrudable-base.js";
4
4
  export declare class Projection extends ExtrudableGeometryBase {
5
5
  private sourceObjects;
6
6
  constructor(sourceObjects: SceneObject[], targetPlane?: PlaneObjectBase);
7
- build(context?: BuildSceneObjectContext): void;
7
+ build(_context?: BuildSceneObjectContext): void;
8
8
  getDependencies(): SceneObject[];
9
9
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
10
10
  compareTo(other: Projection): boolean;
@@ -1,6 +1,8 @@
1
1
  import { Face } from "../../common/face.js";
2
2
  import { Edge } from "../../common/edge.js";
3
3
  import { Vertex } from "../../common/vertex.js";
4
+ import { EdgeOps } from "../../oc/edge-ops.js";
5
+ import { WireOps } from "../../oc/wire-ops.js";
4
6
  import { ProjectionOps } from "../../oc/intersection.js";
5
7
  import { Wire } from "../../common/wire.js";
6
8
  import { ExtrudableGeometryBase } from "./extrudable-base.js";
@@ -10,16 +12,14 @@ export class Projection extends ExtrudableGeometryBase {
10
12
  super(targetPlane);
11
13
  this.sourceObjects = sourceObjects;
12
14
  }
13
- build(context) {
15
+ build(_context) {
14
16
  const plane = this.targetPlane?.getPlane() || this.sketch.getPlane();
15
17
  const shapes = this.sourceObjects.flatMap(obj => obj.getShapes());
16
- const transform = context?.getTransform() ?? null;
17
- let lastWire = null;
18
- console.log('Projection: building with shapes:', shapes.length);
19
- for (let shape of shapes) {
20
- // if (transform) {
21
- // shape = ShapeOps.transform(shape, transform);
22
- // }
18
+ // Project every source first; collect all resulting wires before any dedup
19
+ // or scene-graph mutation. We need the full set up-front so the General Fuse
20
+ // in unifyCoincident can detect overlaps across sources, not just within one.
21
+ const allWires = [];
22
+ for (const shape of shapes) {
23
23
  let wires = [];
24
24
  if (shape instanceof Face) {
25
25
  wires = ProjectionOps.projectFaceOntoPlane(plane, shape);
@@ -31,14 +31,24 @@ export class Projection extends ExtrudableGeometryBase {
31
31
  else if (shape instanceof Edge) {
32
32
  wires = ProjectionOps.projectEdgeOntoPlane(plane, shape);
33
33
  }
34
- for (const wire of wires) {
35
- lastWire = wire;
36
- this.addShapes(wire.getEdges());
37
- }
34
+ allWires.push(...wires);
35
+ }
36
+ // Capture the sketch-cursor endpoints BEFORE dedup. unifyCoincident may
37
+ // split/drop edges and the wire structure is discarded anyway, but the
38
+ // chain endpoints of the first connected group are still the right anchor
39
+ // for the sketch's current position. When multiple disjoint pieces are
40
+ // projected, we use the first as a stable convention.
41
+ const allEdges = allWires.flatMap(w => w.getEdges());
42
+ let endpoints = null;
43
+ if (allEdges.length > 0) {
44
+ const groups = WireOps.groupConnectedEdges(allEdges);
45
+ endpoints = WireOps.findChainEndpoints(groups[0]);
38
46
  }
39
- if (lastWire) {
40
- const localStart = plane.worldToLocal(lastWire.getFirstVertex().toPoint());
41
- const localEnd = plane.worldToLocal(lastWire.getLastVertex().toPoint());
47
+ const uniqueEdges = EdgeOps.unifyCoincident(allEdges);
48
+ this.addShapes(uniqueEdges);
49
+ if (endpoints) {
50
+ const localStart = plane.worldToLocal(endpoints.start.toPoint());
51
+ const localEnd = plane.worldToLocal(endpoints.end.toPoint());
42
52
  this.setState('start', Vertex.fromPoint2D(localStart));
43
53
  this.setState('end', Vertex.fromPoint2D(localEnd));
44
54
  }
@@ -12,7 +12,7 @@ export declare class Sketch extends SceneObject implements Extrudable {
12
12
  isExtrudable(): boolean;
13
13
  getPlane(): Plane;
14
14
  getStartPoint(): Point2D;
15
- getTangentAt(currentObj: GeometrySceneObject): Point2D | null;
15
+ getTangentAt(currentObj: GeometrySceneObject): Point2D;
16
16
  getPositionAt(currentObj: GeometrySceneObject): Point2D;
17
17
  getPreviousPosition(currentObj: GeometrySceneObject, count?: number): Point2D;
18
18
  getLastPosition(scope?: Set<SceneObject>): Point2D;
@@ -24,7 +24,7 @@ export declare class Sketch extends SceneObject implements Extrudable {
24
24
  getDependencies(): SceneObject[];
25
25
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
26
26
  compareTo(other: Sketch): boolean;
27
- getTangent(scope?: Set<SceneObject>): Point2D | null;
27
+ getTangent(scope?: Set<SceneObject>): Point2D;
28
28
  getType(): string;
29
29
  serialize(scope?: Set<SceneObject>): {
30
30
  currentPosition: import("../../math/point.js").Point;
@@ -32,6 +32,11 @@ export class Sketch extends SceneObject {
32
32
  const previous = children.slice(0, children.indexOf(currentObj));
33
33
  let last = previous[previous.length - 1];
34
34
  while (last) {
35
+ if (!(last instanceof GeometrySceneObject)) {
36
+ previous.pop();
37
+ last = previous[previous.length - 1];
38
+ continue;
39
+ }
35
40
  const tangent = last.getTangent();
36
41
  if (tangent) {
37
42
  return tangent;
@@ -39,7 +44,7 @@ export class Sketch extends SceneObject {
39
44
  previous.pop();
40
45
  last = previous[previous.length - 1];
41
46
  }
42
- return null;
47
+ return new Point2D(1, 0);
43
48
  }
44
49
  getPositionAt(currentObj) {
45
50
  const children = this.getChildren();
@@ -94,6 +99,7 @@ export class Sketch extends SceneObject {
94
99
  }
95
100
  }
96
101
  build(context) {
102
+ this.setState('tangent', new Point2D(1, 0));
97
103
  this.planeObj.removeShapes(this);
98
104
  const source = this.getCloneSource();
99
105
  const transform = context?.getTransform();
@@ -174,7 +180,7 @@ export class Sketch extends SceneObject {
174
180
  children = children.filter(c => scope.has(c));
175
181
  }
176
182
  if (children.length === 0) {
177
- return null;
183
+ return new Point2D(1, 0);
178
184
  }
179
185
  let last = children[children.length - 1];
180
186
  while (last) {
@@ -190,7 +196,7 @@ export class Sketch extends SceneObject {
190
196
  children.pop();
191
197
  last = children[children.length - 1];
192
198
  }
193
- return null;
199
+ return new Point2D(1, 0);
194
200
  }
195
201
  getType() {
196
202
  return "sketch";
@@ -200,7 +206,7 @@ export class Sketch extends SceneObject {
200
206
  const tangent = this.getTangent(scope);
201
207
  return {
202
208
  currentPosition: plane.localToWorld(this.getLastPosition(scope)),
203
- currentTangent: tangent ? plane.localToWorld(tangent) : null,
209
+ currentTangent: plane.localToWorld(tangent),
204
210
  plane: this.planeObj.serialize(),
205
211
  };
206
212
  }
@@ -10,9 +10,6 @@ export class TangentArcToPoint extends GeometrySceneObject {
10
10
  }
11
11
  build() {
12
12
  const tangent = this.sketch.getTangentAt(this);
13
- if (!tangent) {
14
- throw new Error('TangentArcToPoint requires a previous sibling with a tangent');
15
- }
16
13
  const plane = this.sketch.getPlane();
17
14
  const startPoint = this.getCurrentPosition();
18
15
  const targetPoint = this.endPoint.asPoint2D();
@@ -13,9 +13,6 @@ export class TangentArc extends GeometrySceneObject {
13
13
  }
14
14
  build() {
15
15
  const tangent = this.sketch.getTangentAt(this);
16
- if (!tangent) {
17
- throw new Error('TangentArc requires a previous sibling with a tangent');
18
- }
19
16
  const plane = this.sketch.getPlane();
20
17
  // Negative radius flips the sweep direction.
21
18
  const radius = Math.abs(this.radius);
@@ -9,9 +9,6 @@ export class TangentLine extends GeometrySceneObject {
9
9
  }
10
10
  build() {
11
11
  const tangent = this.sketch.getTangentAt(this);
12
- if (!tangent) {
13
- throw new Error('TangentLine requires a previous sibling with a tangent');
14
- }
15
12
  const plane = this.sketch.getPlane();
16
13
  const startPoint = this.getCurrentPosition();
17
14
  const start = plane.localToWorld(startPoint);
@@ -38,7 +38,11 @@ export class VerticalLine extends GeometrySceneObject {
38
38
  throw new Error('vLine: .centered() cannot be combined with a target geometry');
39
39
  }
40
40
  startPoint = currentPos;
41
- endPoint = findNearestRayIntersection(plane, startPoint, new Point2D(0, 1), this.distanceOrTarget);
41
+ const hit = findNearestRayIntersection(plane, startPoint, new Point2D(0, 1), this.distanceOrTarget);
42
+ if (!hit) {
43
+ throw new Error("Line does not intersect target geometry");
44
+ }
45
+ endPoint = hit;
42
46
  signedLength = endPoint.y - startPoint.y;
43
47
  }
44
48
  const start = plane.localToWorld(startPoint);
@@ -1,8 +1,8 @@
1
1
  import { SceneObject } from "../../common/scene-object.js";
2
2
  import { GeometrySceneObject } from "./geometry.js";
3
3
  export declare class VMove extends GeometrySceneObject {
4
- distance: number;
5
- constructor(distance: number);
4
+ distanceOrTarget: number | SceneObject;
5
+ constructor(distanceOrTarget: number | SceneObject);
6
6
  getType(): string;
7
7
  build(): void;
8
8
  getDependencies(): SceneObject[];
@@ -1,24 +1,43 @@
1
1
  import { Point2D } from "../../math/point.js";
2
+ import { SceneObject } from "../../common/scene-object.js";
2
3
  import { GeometrySceneObject } from "./geometry.js";
4
+ import { findNearestRayIntersection } from "../../oc/ray-intersect.js";
3
5
  export class VMove extends GeometrySceneObject {
4
- distance;
5
- constructor(distance) {
6
+ distanceOrTarget;
7
+ constructor(distanceOrTarget) {
6
8
  super();
7
- this.distance = distance;
9
+ this.distanceOrTarget = distanceOrTarget;
8
10
  }
9
11
  getType() {
10
12
  return 'vmove';
11
13
  }
12
14
  build() {
13
15
  const pos = this.getCurrentPosition();
14
- const newPos = new Point2D(pos.x, pos.y + this.distance);
16
+ let newPos;
17
+ if (typeof this.distanceOrTarget === 'number') {
18
+ newPos = new Point2D(pos.x, pos.y + this.distanceOrTarget);
19
+ }
20
+ else {
21
+ const plane = this.sketch.getPlane();
22
+ const hit = findNearestRayIntersection(plane, pos, new Point2D(0, 1), this.distanceOrTarget);
23
+ if (!hit) {
24
+ throw new Error("Cannot move vertically up to the specified geometry");
25
+ }
26
+ newPos = hit;
27
+ }
15
28
  this.setCurrentPosition(newPos);
16
29
  }
17
30
  getDependencies() {
31
+ if (this.distanceOrTarget instanceof SceneObject) {
32
+ return [this.distanceOrTarget];
33
+ }
18
34
  return [];
19
35
  }
20
36
  createCopy(remap) {
21
- return new VMove(this.distance);
37
+ const distanceOrTarget = this.distanceOrTarget instanceof SceneObject
38
+ ? (remap.get(this.distanceOrTarget) || this.distanceOrTarget)
39
+ : this.distanceOrTarget;
40
+ return new VMove(distanceOrTarget);
22
41
  }
23
42
  compareTo(other) {
24
43
  if (!(other instanceof VMove)) {
@@ -27,11 +46,17 @@ export class VMove extends GeometrySceneObject {
27
46
  if (!super.compareTo(other)) {
28
47
  return false;
29
48
  }
30
- return this.distance === other.distance;
49
+ if (typeof this.distanceOrTarget !== typeof other.distanceOrTarget) {
50
+ return false;
51
+ }
52
+ if (this.distanceOrTarget instanceof SceneObject && other.distanceOrTarget instanceof SceneObject) {
53
+ return this.distanceOrTarget.compareTo(other.distanceOrTarget);
54
+ }
55
+ return this.distanceOrTarget === other.distanceOrTarget;
31
56
  }
32
57
  serialize() {
33
58
  return {
34
- distance: this.distance
59
+ distance: typeof this.distanceOrTarget === 'number' ? this.distanceOrTarget : null
35
60
  };
36
61
  }
37
62
  }
@@ -5,6 +5,7 @@ export declare class AxisFromEdge extends AxisObjectBase {
5
5
  private sourceObject;
6
6
  private options?;
7
7
  constructor(sourceObject: SceneObject, options?: AxisTransformOptions);
8
+ validate(): void;
8
9
  build(): void;
9
10
  compareTo(other: AxisFromEdge): boolean;
10
11
  getUniqueType(): string;
@@ -2,6 +2,7 @@ import { SelectSceneObject } from "./select.js";
2
2
  import { AxisObjectBase } from "./axis-renderable-base.js";
3
3
  import { EdgeOps } from "../oc/edge-ops.js";
4
4
  import { SceneObject } from "../common/scene-object.js";
5
+ import { requireShapes } from "../common/operand-check.js";
5
6
  export class AxisFromEdge extends AxisObjectBase {
6
7
  sourceObject;
7
8
  options;
@@ -10,6 +11,13 @@ export class AxisFromEdge extends AxisObjectBase {
10
11
  this.sourceObject = sourceObject;
11
12
  this.options = options;
12
13
  }
14
+ validate() {
15
+ // AxisObjectBase sources expose the axis directly — no shapes required.
16
+ if (this.sourceObject instanceof AxisObjectBase) {
17
+ return;
18
+ }
19
+ requireShapes(this.sourceObject, "source", "axis");
20
+ }
13
21
  build() {
14
22
  let axis;
15
23
  if (this.sourceObject instanceof SelectSceneObject) {
@@ -6,6 +6,7 @@ export declare class Chamfer extends SceneObject {
6
6
  private _selections;
7
7
  constructor(distance: number, distance2: number, isAngle?: boolean, ...selections: SceneObject[]);
8
8
  get selections(): SceneObject[];
9
+ validate(): void;
9
10
  build(context: BuildSceneObjectContext): void;
10
11
  getDependencies(): SceneObject[];
11
12
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
@@ -3,6 +3,7 @@ import { FilletOps } from "../oc/fillet-ops.js";
3
3
  import { Explorer } from "../oc/explorer.js";
4
4
  import { ShapeOps } from "../oc/shape-ops.js";
5
5
  import { ColorTransfer } from "../oc/color-transfer.js";
6
+ import { requireShapes } from "../common/operand-check.js";
6
7
  export class Chamfer extends SceneObject {
7
8
  distance;
8
9
  distance2;
@@ -18,6 +19,11 @@ export class Chamfer extends SceneObject {
18
19
  get selections() {
19
20
  return this._selections;
20
21
  }
22
+ validate() {
23
+ for (let i = 0; i < this._selections.length; i++) {
24
+ requireShapes(this._selections[i], `edge selection ${i + 1}`, "chamfer");
25
+ }
26
+ }
21
27
  build(context) {
22
28
  let sceneObjects;
23
29
  sceneObjects = new Map();
@@ -4,6 +4,7 @@ export declare class Color extends SceneObject {
4
4
  private _selection;
5
5
  constructor(color: string, selection?: SceneObject);
6
6
  get selection(): SceneObject;
7
+ validate(): void;
7
8
  build(context: BuildSceneObjectContext): void;
8
9
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
9
10
  compareTo(other: Color): boolean;
@@ -1,5 +1,6 @@
1
1
  import { SceneObject } from "../common/scene-object.js";
2
2
  import { SelectSceneObject } from "./select.js";
3
+ import { requireShapes } from "../common/operand-check.js";
3
4
  import cssColorNames from "color-name";
4
5
  function toHex(color) {
5
6
  if (color.startsWith('#')) {
@@ -23,6 +24,11 @@ export class Color extends SceneObject {
23
24
  get selection() {
24
25
  return this._selection;
25
26
  }
27
+ validate() {
28
+ if (this._selection) {
29
+ requireShapes(this._selection, "selection", "color");
30
+ }
31
+ }
26
32
  build(context) {
27
33
  const sceneObjects = context.getSceneObjects();
28
34
  const objShapeMap = new Map();
@@ -5,6 +5,7 @@ export declare class Common extends SceneObject {
5
5
  constructor(...objects: SceneObject[]);
6
6
  keepOriginal(value?: boolean): this;
7
7
  get sceneObjects(): SceneObject[];
8
+ validate(): void;
8
9
  build(context: BuildSceneObjectContext): void;
9
10
  compareTo(other: Common): boolean;
10
11
  getType(): string;
@@ -1,5 +1,6 @@
1
1
  import { SceneObject } from "../common/scene-object.js";
2
2
  import { BooleanOps } from "../oc/boolean-ops.js";
3
+ import { requireShapes } from "../common/operand-check.js";
3
4
  export class Common extends SceneObject {
4
5
  _sceneObjects = [];
5
6
  _keepOriginal = false;
@@ -14,6 +15,14 @@ export class Common extends SceneObject {
14
15
  get sceneObjects() {
15
16
  return this._sceneObjects;
16
17
  }
18
+ validate() {
19
+ // Scene-wide mode (no explicit operands) has nothing to fault — let build
20
+ // pick up whatever is in the scene. Only validate when the user named
21
+ // specific operands.
22
+ for (let i = 0; i < this._sceneObjects.length; i++) {
23
+ requireShapes(this._sceneObjects[i], `operand ${i + 1}`, "common");
24
+ }
25
+ }
17
26
  build(context) {
18
27
  const p = context.getProfiler();
19
28
  let sceneObjects = this.sceneObjects;
@@ -6,6 +6,7 @@ export declare class Common2D extends GeometrySceneObject {
6
6
  constructor(...targets: GeometrySceneObject[]);
7
7
  keepOriginal(value?: boolean): this;
8
8
  get targetObjects(): GeometrySceneObject[] | null;
9
+ validate(): void;
9
10
  build(context: BuildSceneObjectContext): void;
10
11
  getDependencies(): SceneObject[];
11
12
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
@@ -3,6 +3,7 @@ import { Edge } from "../common/edge.js";
3
3
  import { GeometrySceneObject } from "./2d/geometry.js";
4
4
  import { BooleanOps } from "../oc/boolean-ops.js";
5
5
  import { FaceMaker2 } from "../oc/face-maker2.js";
6
+ import { requireShapes } from "../common/operand-check.js";
6
7
  export class Common2D extends GeometrySceneObject {
7
8
  _targetObjects = null;
8
9
  _keepOriginal = false;
@@ -17,6 +18,14 @@ export class Common2D extends GeometrySceneObject {
17
18
  get targetObjects() {
18
19
  return this._targetObjects;
19
20
  }
21
+ validate() {
22
+ if (!this._targetObjects) {
23
+ return;
24
+ }
25
+ for (let i = 0; i < this._targetObjects.length; i++) {
26
+ requireShapes(this._targetObjects[i], `operand ${i + 1}`, "common2d");
27
+ }
28
+ }
20
29
  build(context) {
21
30
  let sourceEdges;
22
31
  if (this._targetObjects === null) {
@@ -4,6 +4,7 @@ export declare class Draft extends SceneObject implements IDraft {
4
4
  private angle;
5
5
  private _selections;
6
6
  constructor(angle: number, selections: SceneObject[]);
7
+ validate(): void;
7
8
  build(context: BuildSceneObjectContext): void;
8
9
  compareTo(other: SceneObject): boolean;
9
10
  getDependencies(): SceneObject[];
@@ -1,5 +1,6 @@
1
1
  import { SceneObject } from "../common/scene-object.js";
2
2
  import { DraftOps } from "../oc/draft-ops.js";
3
+ import { requireShapes } from "../common/operand-check.js";
3
4
  export class Draft extends SceneObject {
4
5
  angle;
5
6
  _selections = [];
@@ -8,6 +9,11 @@ export class Draft extends SceneObject {
8
9
  this.angle = angle;
9
10
  this._selections = selections;
10
11
  }
12
+ validate() {
13
+ for (let i = 0; i < this._selections.length; i++) {
14
+ requireShapes(this._selections[i], `selection ${i + 1}`, "draft");
15
+ }
16
+ }
11
17
  build(context) {
12
18
  const shapeObjMap = new Map();
13
19
  for (const obj of context.getSceneObjects()) {