fluidcad 0.0.30 → 0.0.31

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 (105) 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/interfaces.d.ts +1 -1
  18. package/lib/dist/features/2d/aline.js +6 -2
  19. package/lib/dist/features/2d/arc.d.ts +3 -0
  20. package/lib/dist/features/2d/arc.js +28 -1
  21. package/lib/dist/features/2d/hline.js +5 -1
  22. package/lib/dist/features/2d/hmove.d.ts +2 -2
  23. package/lib/dist/features/2d/hmove.js +32 -7
  24. package/lib/dist/features/2d/intersect.js +17 -10
  25. package/lib/dist/features/2d/pmove.d.ts +2 -2
  26. package/lib/dist/features/2d/pmove.js +47 -7
  27. package/lib/dist/features/2d/projection.d.ts +1 -1
  28. package/lib/dist/features/2d/projection.js +25 -15
  29. package/lib/dist/features/2d/sketch.d.ts +2 -2
  30. package/lib/dist/features/2d/sketch.js +10 -4
  31. package/lib/dist/features/2d/tarc-to-point.js +0 -3
  32. package/lib/dist/features/2d/tarc.js +0 -3
  33. package/lib/dist/features/2d/tline.js +0 -3
  34. package/lib/dist/features/2d/vline.js +5 -1
  35. package/lib/dist/features/2d/vmove.d.ts +2 -2
  36. package/lib/dist/features/2d/vmove.js +32 -7
  37. package/lib/dist/features/axis-from-edge.d.ts +1 -0
  38. package/lib/dist/features/axis-from-edge.js +8 -0
  39. package/lib/dist/features/chamfer.d.ts +1 -0
  40. package/lib/dist/features/chamfer.js +6 -0
  41. package/lib/dist/features/color.d.ts +1 -0
  42. package/lib/dist/features/color.js +6 -0
  43. package/lib/dist/features/common.d.ts +1 -0
  44. package/lib/dist/features/common.js +9 -0
  45. package/lib/dist/features/common2d.d.ts +1 -0
  46. package/lib/dist/features/common2d.js +9 -0
  47. package/lib/dist/features/draft.d.ts +1 -0
  48. package/lib/dist/features/draft.js +6 -0
  49. package/lib/dist/features/fillet.d.ts +1 -0
  50. package/lib/dist/features/fillet.js +6 -0
  51. package/lib/dist/features/fillet2d.d.ts +1 -0
  52. package/lib/dist/features/fillet2d.js +9 -0
  53. package/lib/dist/features/fuse.d.ts +1 -0
  54. package/lib/dist/features/fuse.js +6 -0
  55. package/lib/dist/features/fuse2d.d.ts +1 -0
  56. package/lib/dist/features/fuse2d.js +9 -0
  57. package/lib/dist/features/loft.d.ts +1 -0
  58. package/lib/dist/features/loft.js +6 -0
  59. package/lib/dist/features/mirror-shape.d.ts +1 -0
  60. package/lib/dist/features/mirror-shape.js +28 -8
  61. package/lib/dist/features/plane-from-object.d.ts +1 -0
  62. package/lib/dist/features/plane-from-object.js +8 -0
  63. package/lib/dist/features/rotate.d.ts +1 -0
  64. package/lib/dist/features/rotate.js +9 -0
  65. package/lib/dist/features/shell.d.ts +1 -0
  66. package/lib/dist/features/shell.js +6 -0
  67. package/lib/dist/features/subtract.d.ts +1 -0
  68. package/lib/dist/features/subtract.js +5 -0
  69. package/lib/dist/features/subtract2d.d.ts +1 -0
  70. package/lib/dist/features/subtract2d.js +5 -0
  71. package/lib/dist/features/sweep.d.ts +1 -0
  72. package/lib/dist/features/sweep.js +4 -0
  73. package/lib/dist/features/translate.d.ts +1 -0
  74. package/lib/dist/features/translate.js +9 -0
  75. package/lib/dist/oc/boolean-ops.d.ts +2 -2
  76. package/lib/dist/oc/edge-ops.d.ts +17 -0
  77. package/lib/dist/oc/edge-ops.js +60 -0
  78. package/lib/dist/oc/face-query.js +17 -13
  79. package/lib/dist/oc/ray-intersect.d.ts +3 -2
  80. package/lib/dist/oc/ray-intersect.js +2 -4
  81. package/lib/dist/oc/shell-ops.js +15 -2
  82. package/lib/dist/oc/thin-face-maker.d.ts +15 -0
  83. package/lib/dist/oc/thin-face-maker.js +48 -7
  84. package/lib/dist/oc/wire-ops.d.ts +14 -0
  85. package/lib/dist/oc/wire-ops.js +38 -0
  86. package/lib/dist/rendering/render.js +6 -4
  87. package/lib/dist/tests/common/describe-error.test.d.ts +1 -0
  88. package/lib/dist/tests/common/describe-error.test.js +36 -0
  89. package/lib/dist/tests/features/2d/intersect.test.js +43 -0
  90. package/lib/dist/tests/features/2d/move.test.js +72 -1
  91. package/lib/dist/tests/features/2d/project-regression.test.js +35 -0
  92. package/lib/dist/tests/features/color-lineage.test.js +24 -0
  93. package/lib/dist/tests/features/cylinder-curve-filter.test.d.ts +1 -0
  94. package/lib/dist/tests/features/cylinder-curve-filter.test.js +99 -0
  95. package/lib/dist/tests/features/mirror.test.js +74 -0
  96. package/lib/dist/tests/features/subtract-consumed-input.test.d.ts +1 -0
  97. package/lib/dist/tests/features/subtract-consumed-input.test.js +28 -0
  98. package/lib/dist/tests/features/thin-extrude-offset-fix.test.d.ts +1 -0
  99. package/lib/dist/tests/features/thin-extrude-offset-fix.test.js +34 -0
  100. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +2 -3
  102. package/ui/dist/assets/{index-6Ep4GPxf.js → index-B15vMQZ2.js} +102 -102
  103. package/ui/dist/assets/index-DR7c2Qk9.css +2 -0
  104. package/ui/dist/index.html +2 -2
  105. package/ui/dist/assets/index-DRKfe6N9.css +0 -2
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Error thrown by feature pre-build validation when an operand or input is
3
+ * unsuitable for the operation. Carries an optional hint so the user can be
4
+ * told both what went wrong and how to fix it.
5
+ *
6
+ * Extends Error so the renderer's existing try/catch in `buildObject` and
7
+ * `describeError` keep working unchanged.
8
+ */
9
+ export declare class BuildError extends Error {
10
+ readonly diagnostic: string;
11
+ readonly hint?: string;
12
+ constructor(diagnostic: string, hint?: string);
13
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Error thrown by feature pre-build validation when an operand or input is
3
+ * unsuitable for the operation. Carries an optional hint so the user can be
4
+ * told both what went wrong and how to fix it.
5
+ *
6
+ * Extends Error so the renderer's existing try/catch in `buildObject` and
7
+ * `describeError` keep working unchanged.
8
+ */
9
+ export class BuildError extends Error {
10
+ diagnostic;
11
+ hint;
12
+ constructor(diagnostic, hint) {
13
+ super(hint ? `${diagnostic}\nHint: ${hint}` : diagnostic);
14
+ this.diagnostic = diagnostic;
15
+ this.hint = hint;
16
+ this.name = 'BuildError';
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Convert any thrown value to a readable string. OCC throws come through
3
+ * Emscripten as raw numeric pointers; decode those via OCJS.getStandard_FailureData
4
+ * so the actual reason ("Offset wire is not closed.", etc.) reaches the user.
5
+ */
6
+ export declare function describeError(error: unknown): string;
@@ -0,0 +1,26 @@
1
+ import { getOC } from "../oc/init.js";
2
+ /**
3
+ * Convert any thrown value to a readable string. OCC throws come through
4
+ * Emscripten as raw numeric pointers; decode those via OCJS.getStandard_FailureData
5
+ * so the actual reason ("Offset wire is not closed.", etc.) reaches the user.
6
+ */
7
+ export function describeError(error) {
8
+ if (error instanceof Error) {
9
+ return error.message;
10
+ }
11
+ if (typeof error === 'number' && Number.isFinite(error)) {
12
+ try {
13
+ const oc = getOC();
14
+ const failure = oc.OCJS.getStandard_FailureData(error);
15
+ const msg = failure.GetMessageString();
16
+ if (msg) {
17
+ return `OCC: ${msg}`;
18
+ }
19
+ return `OCC error (ptr=${error})`;
20
+ }
21
+ catch (e) {
22
+ console.log("[describeError] decode failed:", e);
23
+ }
24
+ }
25
+ return String(error);
26
+ }
@@ -0,0 +1,19 @@
1
+ import { SceneObject } from "./scene-object.js";
2
+ import { Shape } from "./shape.js";
3
+ interface RequireShapesOpts {
4
+ /** Require exactly this many shapes. */
5
+ count?: number;
6
+ /** Require every shape to be of this type. */
7
+ type?: string;
8
+ }
9
+ /**
10
+ * Validate that an operand SceneObject still owns shapes the consumer can use,
11
+ * and produce a uniform diagnostic when it doesn't.
12
+ *
13
+ * The most common failure mode is a SceneObject whose geometry was consumed by
14
+ * an earlier op (e.g. `translate(amount, target)` moves the shape from
15
+ * `target` onto the translate object). `getRemovedShapes()` records who took
16
+ * each shape, so we can name the consumer in the error.
17
+ */
18
+ export declare function requireShapes(obj: SceneObject, operandLabel: string, consumerType: string, opts?: RequireShapesOpts): Shape[];
19
+ export {};
@@ -0,0 +1,38 @@
1
+ import { BuildError } from "./build-error.js";
2
+ /**
3
+ * Validate that an operand SceneObject still owns shapes the consumer can use,
4
+ * and produce a uniform diagnostic when it doesn't.
5
+ *
6
+ * The most common failure mode is a SceneObject whose geometry was consumed by
7
+ * an earlier op (e.g. `translate(amount, target)` moves the shape from
8
+ * `target` onto the translate object). `getRemovedShapes()` records who took
9
+ * each shape, so we can name the consumer in the error.
10
+ */
11
+ export function requireShapes(obj, operandLabel, consumerType, opts) {
12
+ // Lazy operands (LazySelectionSceneObject, LazyVertex) only populate their
13
+ // shapes during build. Their pre-build emptiness is expected, so skip —
14
+ // the build itself still validates them.
15
+ if (obj.isLazy()) {
16
+ return [];
17
+ }
18
+ const shapes = obj.getShapes();
19
+ if (shapes.length === 0) {
20
+ const removed = obj.getRemovedShapes();
21
+ const objLabel = `${operandLabel} (${obj.getType()})`;
22
+ if (removed.length > 0) {
23
+ const consumers = [...new Set(removed.map(r => r.removedBy.getType()))].join(", ");
24
+ throw new BuildError(`${consumerType}: ${objLabel} has no shapes — its geometry was consumed by ${consumers}.`, `Reference the result of ${consumers} as the operand instead of ${obj.getType()}.`);
25
+ }
26
+ throw new BuildError(`${consumerType}: ${objLabel} has no shapes.`, `Make sure the upstream operation produced geometry.`);
27
+ }
28
+ if (opts?.count !== undefined && shapes.length !== opts.count) {
29
+ throw new BuildError(`${consumerType}: ${operandLabel} has ${shapes.length} shapes, expected ${opts.count}.`);
30
+ }
31
+ if (opts?.type) {
32
+ const wrong = shapes.find(s => s.getType() !== opts.type);
33
+ if (wrong) {
34
+ throw new BuildError(`${consumerType}: ${operandLabel} has a shape of type '${wrong.getType()}', expected '${opts.type}'.`);
35
+ }
36
+ }
37
+ return shapes;
38
+ }
@@ -75,6 +75,14 @@ export declare abstract class SceneObject implements Comparable<SceneObject>, Se
75
75
  abstract serialize(scope?: Set<SceneObject>): any;
76
76
  abstract getType(): string;
77
77
  abstract build(context?: BuildSceneObjectContext): void;
78
+ /**
79
+ * Pre-build validation hook. The renderer calls this before `build()` so
80
+ * features can fail fast with a clear diagnostic (typically a `BuildError`)
81
+ * before any OC work runs. Default is a no-op; overrides should not mutate
82
+ * state. Existing per-feature checks inside `build()` are intentionally not
83
+ * removed — this hook is additive.
84
+ */
85
+ validate(): void;
78
86
  getAppliedTransform(): Matrix4 | null;
79
87
  protected composeAppliedTransform(matrix: Matrix4): void;
80
88
  compareTo(other: SceneObject): boolean;
@@ -100,6 +100,16 @@ export class SceneObject {
100
100
  getSnapshot() {
101
101
  return this.getState('snapshot') || [];
102
102
  }
103
+ /**
104
+ * Pre-build validation hook. The renderer calls this before `build()` so
105
+ * features can fail fast with a clear diagnostic (typically a `BuildError`)
106
+ * before any OC work runs. Default is a no-op; overrides should not mutate
107
+ * state. Existing per-feature checks inside `build()` are intentionally not
108
+ * removed — this hook is additive.
109
+ */
110
+ validate() {
111
+ // Override in subclasses to add operand checks via `requireShapes` etc.
112
+ }
103
113
  getAppliedTransform() {
104
114
  return this._appliedTransform;
105
115
  }
@@ -4,5 +4,5 @@ import { Wire } from "./wire.js";
4
4
  import { Face } from "./face.js";
5
5
  import { Edge } from "./edge.js";
6
6
  export declare class ShapeFactory {
7
- static fromShape(shape: TopoDS_Shape): Solid | Face | Edge | Wire;
7
+ static fromShape(shape: TopoDS_Shape): Edge | Wire | Solid | Face;
8
8
  }
@@ -18,10 +18,12 @@ interface ArcFunction {
18
18
  (startPoint: Point2DLike, endPoint: Point2DLike): IArcPoints;
19
19
  /**
20
20
  * Draws an arc by radius and angle range at the current position.
21
+ * Angles are measured relative to the current tangent (the tangent of the previous
22
+ * geometry, or +X when there is none).
21
23
  * Chain `.centered()` to center the arc symmetrically around the start angle.
22
24
  * @param radius - The arc radius
23
- * @param startAngle - The start angle in degrees (defaults to 0)
24
- * @param endAngle - The end angle in degrees (defaults to 180)
25
+ * @param startAngle - The start angle in degrees, relative to the current tangent (defaults to 0)
26
+ * @param endAngle - The end angle in degrees, relative to the current tangent (defaults to 180)
25
27
  */
26
28
  (radius: number, startAngle?: number, endAngle?: number): IArcAngles;
27
29
  /**
@@ -1,9 +1,16 @@
1
+ import { IGeometry, ISceneObject } from "../interfaces.js";
1
2
  interface HMoveFunction {
2
3
  /**
3
4
  * Moves the cursor horizontally by the given distance.
4
5
  * @param distance - The horizontal distance to move
5
6
  */
6
- (distance: number): void;
7
+ (distance: number): IGeometry;
8
+ /**
9
+ * Moves the cursor horizontally to the nearest intersection with the target geometry.
10
+ * The nearest intersection (in either direction along the X axis) is used.
11
+ * @param target - The geometry to intersect with
12
+ */
13
+ (target: ISceneObject): IGeometry;
7
14
  }
8
15
  declare const _default: HMoveFunction;
9
16
  export default _default;
@@ -1,9 +1,13 @@
1
1
  import { HMove } from "../../features/2d/hmove.js";
2
+ import { SceneObject } from "../../common/scene-object.js";
2
3
  import { registerBuilder } from "../../index.js";
3
4
  function build(context) {
4
5
  return function move() {
5
- const distance = arguments[0];
6
- const hmove = new HMove(distance);
6
+ const arg = arguments[0];
7
+ const distanceOrTarget = arg instanceof SceneObject
8
+ ? arg
9
+ : arg;
10
+ const hmove = new HMove(distanceOrTarget);
7
11
  context.addSceneObject(hmove);
8
12
  return hmove;
9
13
  };
@@ -1,11 +1,21 @@
1
- import { IGeometry } from "../interfaces.js";
1
+ import { IGeometry, ISceneObject } from "../interfaces.js";
2
2
  interface PolarMoveFunction {
3
3
  /**
4
- * Moves the cursor by polar coordinates.
4
+ * Moves the cursor by polar coordinates, relative to the current tangent.
5
+ * The angle is measured from the tangent of the previous geometry (defaults to +X
6
+ * when there is none).
5
7
  * @param radius - The distance to move
6
- * @param angle - The angle in degrees
8
+ * @param angle - The angle in degrees, relative to the current tangent
7
9
  */
8
10
  (radius: number, angle: number): IGeometry;
11
+ /**
12
+ * Moves the cursor along the given angle (relative to the current tangent) to the
13
+ * nearest intersection with the target geometry. The nearest intersection (in either
14
+ * direction along the ray) is used.
15
+ * @param target - The geometry to intersect with
16
+ * @param angle - The angle in degrees, relative to the current tangent
17
+ */
18
+ (target: ISceneObject, angle: number): IGeometry;
9
19
  }
10
20
  declare const _default: PolarMoveFunction;
11
21
  export default _default;
@@ -1,10 +1,14 @@
1
1
  import { PolarMove } from "../../features/2d/pmove.js";
2
+ import { SceneObject } from "../../common/scene-object.js";
2
3
  import { registerBuilder } from "../../index.js";
3
4
  function build(context) {
4
5
  return function pmove() {
5
- const radius = arguments[0];
6
+ const arg0 = arguments[0];
6
7
  const angle = arguments[1] * Math.PI / 180;
7
- const pmove = new PolarMove(radius, angle);
8
+ const radiusOrTarget = arg0 instanceof SceneObject
9
+ ? arg0
10
+ : arg0;
11
+ const pmove = new PolarMove(radiusOrTarget, angle);
8
12
  context.addSceneObject(pmove);
9
13
  return pmove;
10
14
  };
@@ -1,9 +1,16 @@
1
+ import { IGeometry, ISceneObject } from "../interfaces.js";
1
2
  interface VMoveFunction {
2
3
  /**
3
4
  * Moves the cursor vertically by the given distance.
4
5
  * @param distance - The vertical distance to move
5
6
  */
6
- (distance: number): void;
7
+ (distance: number): IGeometry;
8
+ /**
9
+ * Moves the cursor vertically to the nearest intersection with the target geometry.
10
+ * The nearest intersection (in either direction along the Y axis) is used.
11
+ * @param target - The geometry to intersect with
12
+ */
13
+ (target: ISceneObject): IGeometry;
7
14
  }
8
15
  declare const _default: VMoveFunction;
9
16
  export default _default;
@@ -1,11 +1,15 @@
1
1
  import { VMove } from "../../features/2d/vmove.js";
2
+ import { SceneObject } from "../../common/scene-object.js";
2
3
  import { registerBuilder } from "../../index.js";
3
4
  function build(context) {
4
5
  return function move() {
5
- const distance = arguments[0];
6
- const move = new VMove(distance);
7
- context.addSceneObject(move);
8
- return move;
6
+ const arg = arguments[0];
7
+ const distanceOrTarget = arg instanceof SceneObject
8
+ ? arg
9
+ : arg;
10
+ const vmove = new VMove(distanceOrTarget);
11
+ context.addSceneObject(vmove);
12
+ return vmove;
9
13
  };
10
14
  }
11
15
  export default registerBuilder(build);
@@ -656,7 +656,7 @@ export interface ISweep extends IBooleanOperation {
656
656
  */
657
657
  capEdges(...args: (number | EdgeFilterBuilder)[]): ISceneObject;
658
658
  }
659
- export interface IMirror extends ISceneObject {
659
+ export interface IMirror extends IBooleanOperation {
660
660
  /**
661
661
  * Excludes the given objects from the mirror operation. Useful when
662
662
  * mirroring "everything" but a few specific objects should be skipped,
@@ -22,7 +22,7 @@ export class AngledLine extends GeometrySceneObject {
22
22
  }
23
23
  build() {
24
24
  const plane = this.targetPlane?.getPlane() || this.sketch.getPlane();
25
- let tangent = this.sketch?.getTangentAt(this) || new Point2D(1, 0);
25
+ let tangent = this.sketch?.getTangentAt(this) ?? new Point2D(1, 0);
26
26
  tangent = tangent.normalize();
27
27
  const angleRad = rad(this.angle);
28
28
  // 2D rotation of tangent by angle
@@ -48,7 +48,11 @@ export class AngledLine extends GeometrySceneObject {
48
48
  throw new Error('aLine: .centered() cannot be combined with a target geometry');
49
49
  }
50
50
  startPoint = currentPos;
51
- endPoint = findNearestRayIntersection(plane, startPoint, direction, this.lengthOrTarget);
51
+ const hit = findNearestRayIntersection(plane, startPoint, direction, this.lengthOrTarget);
52
+ if (!hit) {
53
+ throw new Error("Line does not intersect target geometry");
54
+ }
55
+ endPoint = hit;
52
56
  }
53
57
  const start = plane.localToWorld(startPoint);
54
58
  const end = plane.localToWorld(endPoint);
@@ -3,6 +3,7 @@ import { PlaneObjectBase } from "../plane-renderable-base.js";
3
3
  import { GeometrySceneObject } from "./geometry.js";
4
4
  import { LazyVertex } from "../lazy-vertex.js";
5
5
  import { IArcPoints, IArcAngles } from "../../core/interfaces.js";
6
+ import { SceneObject } from "../../common/scene-object.js";
6
7
  export declare class Arc extends GeometrySceneObject implements IArcPoints, IArcAngles {
7
8
  private _startPoint;
8
9
  private _endPoint;
@@ -27,6 +28,8 @@ export declare class Arc extends GeometrySceneObject implements IArcPoints, IArc
27
28
  private buildWithCenter;
28
29
  private buildFromAngles;
29
30
  getType(): string;
31
+ getDependencies(): SceneObject[];
32
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
30
33
  compareTo(other: Arc): boolean;
31
34
  serialize(): Record<string, unknown>;
32
35
  }
@@ -167,7 +167,6 @@ export class Arc extends GeometrySceneObject {
167
167
  const d = Math.sqrt(r * r - (chordLen / 2) * (chordLen / 2));
168
168
  const sign = cw ? -1 : 1;
169
169
  const centerPoint = new Point2D(mx + sign * d * px, my + sign * d * py);
170
- const startAngle = Math.atan2(startPoint.y - centerPoint.y, startPoint.x - centerPoint.x);
171
170
  const endAngle = Math.atan2(targetPoint.y - centerPoint.y, targetPoint.x - centerPoint.x);
172
171
  const normal = cw ? plane.normal.negate() : plane.normal;
173
172
  const center = plane.localToWorld(centerPoint);
@@ -224,6 +223,10 @@ export class Arc extends GeometrySceneObject {
224
223
  const centerPoint = this._targetPlane
225
224
  ? plane.worldToLocal(this._targetPlane.getPlaneCenter())
226
225
  : this.getCurrentPosition();
226
+ // Angles are measured relative to the current tangent (defaults to +X).
227
+ const tangent = (this._targetPlane ? null : this.sketch?.getTangentAt(this))
228
+ ?? new Point2D(1, 0);
229
+ const tangentAngle = Math.atan2(tangent.y, tangent.x);
227
230
  const cw = this._endAngle < 0;
228
231
  const absStartAngle = Math.abs(this._startAngle);
229
232
  const absEndAngle = Math.abs(this._endAngle);
@@ -239,6 +242,8 @@ export class Arc extends GeometrySceneObject {
239
242
  startAngleRad = rad(absStartAngle);
240
243
  endAngleRad = rad(absEndAngle);
241
244
  }
245
+ startAngleRad += tangentAngle;
246
+ endAngleRad += tangentAngle;
242
247
  const normal = cw ? plane.normal.negate() : plane.normal;
243
248
  const startPoint = Geometry.getPointOnCircle(centerPoint, radius, startAngleRad);
244
249
  const endPoint = Geometry.getPointOnCircle(centerPoint, radius, endAngleRad);
@@ -261,6 +266,28 @@ export class Arc extends GeometrySceneObject {
261
266
  getType() {
262
267
  return 'arc';
263
268
  }
269
+ getDependencies() {
270
+ return this._targetPlane ? [this._targetPlane] : [];
271
+ }
272
+ createCopy(remap) {
273
+ const targetPlane = this._targetPlane
274
+ ? (remap.get(this._targetPlane) || this._targetPlane)
275
+ : null;
276
+ let copy;
277
+ if (this._startPoint && this._endPoint) {
278
+ copy = Arc.twoPoints(this._startPoint, this._endPoint, targetPlane);
279
+ }
280
+ else if (this._endPoint) {
281
+ copy = Arc.toPoint(this._endPoint, targetPlane);
282
+ }
283
+ else {
284
+ copy = Arc.fromAngles(this._arcRadius, this._startAngle, this._endAngle, targetPlane);
285
+ }
286
+ copy._bulgeRadius = this._bulgeRadius;
287
+ copy._centerPoint = this._centerPoint;
288
+ copy._centered = this._centered;
289
+ return copy;
290
+ }
264
291
  compareTo(other) {
265
292
  if (!(other instanceof Arc)) {
266
293
  return false;
@@ -38,7 +38,11 @@ export class HorizontalLine extends GeometrySceneObject {
38
38
  throw new Error('hLine: .centered() cannot be combined with a target geometry');
39
39
  }
40
40
  startPoint = currentPos;
41
- endPoint = findNearestRayIntersection(plane, startPoint, new Point2D(1, 0), this.distanceOrTarget);
41
+ const hit = findNearestRayIntersection(plane, startPoint, new Point2D(1, 0), this.distanceOrTarget);
42
+ if (!hit) {
43
+ throw new Error("Line does not intersect target geometry");
44
+ }
45
+ endPoint = hit;
42
46
  signedLength = endPoint.x - startPoint.x;
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 HMove 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 HMove 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 'hmove';
11
13
  }
12
14
  build() {
13
15
  const pos = this.getCurrentPosition();
14
- const newPos = new Point2D(pos.x + this.distance, pos.y);
16
+ let newPos;
17
+ if (typeof this.distanceOrTarget === 'number') {
18
+ newPos = new Point2D(pos.x + this.distanceOrTarget, pos.y);
19
+ }
20
+ else {
21
+ const plane = this.sketch.getPlane();
22
+ const hit = findNearestRayIntersection(plane, pos, new Point2D(1, 0), this.distanceOrTarget);
23
+ if (!hit) {
24
+ throw new Error("Cannot move horizontally 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 HMove(this.distance);
37
+ const distanceOrTarget = this.distanceOrTarget instanceof SceneObject
38
+ ? (remap.get(this.distanceOrTarget) || this.distanceOrTarget)
39
+ : this.distanceOrTarget;
40
+ return new HMove(distanceOrTarget);
22
41
  }
23
42
  compareTo(other) {
24
43
  if (!(other instanceof HMove)) {
@@ -27,11 +46,17 @@ export class HMove 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
  }
@@ -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,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
  }