fluidcad 0.0.28 → 0.0.30

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 (145) hide show
  1. package/lib/dist/common/profiler.d.ts +12 -0
  2. package/lib/dist/common/profiler.js +35 -0
  3. package/lib/dist/common/scene-object.d.ts +3 -0
  4. package/lib/dist/common/scene-object.js +3 -0
  5. package/lib/dist/common/shape-history-tracker.d.ts +9 -1
  6. package/lib/dist/common/shape-history-tracker.js +37 -23
  7. package/lib/dist/core/2d/aline.d.ts +13 -13
  8. package/lib/dist/core/2d/aline.js +20 -11
  9. package/lib/dist/core/2d/arc.d.ts +6 -6
  10. package/lib/dist/core/2d/arc.js +19 -15
  11. package/lib/dist/core/2d/back.d.ts +12 -0
  12. package/lib/dist/core/2d/back.js +11 -0
  13. package/lib/dist/core/2d/circle.d.ts +2 -2
  14. package/lib/dist/core/2d/circle.js +14 -10
  15. package/lib/dist/core/2d/ellipse.d.ts +35 -0
  16. package/lib/dist/core/2d/ellipse.js +65 -0
  17. package/lib/dist/core/2d/hline.d.ts +20 -13
  18. package/lib/dist/core/2d/hline.js +33 -15
  19. package/lib/dist/core/2d/index.d.ts +2 -0
  20. package/lib/dist/core/2d/index.js +2 -0
  21. package/lib/dist/core/2d/intersect.d.ts +2 -2
  22. package/lib/dist/core/2d/intersect.js +7 -3
  23. package/lib/dist/core/2d/line.d.ts +2 -2
  24. package/lib/dist/core/2d/line.js +14 -10
  25. package/lib/dist/core/2d/offset.d.ts +4 -4
  26. package/lib/dist/core/2d/offset.js +9 -5
  27. package/lib/dist/core/2d/polygon.d.ts +4 -4
  28. package/lib/dist/core/2d/polygon.js +24 -20
  29. package/lib/dist/core/2d/project.d.ts +2 -2
  30. package/lib/dist/core/2d/project.js +7 -3
  31. package/lib/dist/core/2d/rect.d.ts +2 -2
  32. package/lib/dist/core/2d/rect.js +22 -21
  33. package/lib/dist/core/2d/slot.d.ts +6 -6
  34. package/lib/dist/core/2d/slot.js +29 -32
  35. package/lib/dist/core/2d/vline.d.ts +20 -13
  36. package/lib/dist/core/2d/vline.js +29 -15
  37. package/lib/dist/core/interfaces.d.ts +62 -0
  38. package/lib/dist/core/mirror.d.ts +7 -7
  39. package/lib/dist/core/mirror.js +17 -11
  40. package/lib/dist/core/part.d.ts +3 -1
  41. package/lib/dist/core/part.js +1 -1
  42. package/lib/dist/core/rotate.d.ts +5 -5
  43. package/lib/dist/core/rotate.js +4 -1
  44. package/lib/dist/core/sketch.d.ts +3 -1
  45. package/lib/dist/core/sketch.js +1 -1
  46. package/lib/dist/core/translate.d.ts +9 -9
  47. package/lib/dist/features/2d/aline.d.ts +8 -5
  48. package/lib/dist/features/2d/aline.js +70 -18
  49. package/lib/dist/features/2d/back.d.ts +14 -0
  50. package/lib/dist/features/2d/back.js +35 -0
  51. package/lib/dist/features/2d/ellipse.d.ts +23 -0
  52. package/lib/dist/features/2d/ellipse.js +75 -0
  53. package/lib/dist/features/2d/hline.d.ts +9 -4
  54. package/lib/dist/features/2d/hline.js +65 -14
  55. package/lib/dist/features/2d/offset.d.ts +3 -0
  56. package/lib/dist/features/2d/offset.js +27 -3
  57. package/lib/dist/features/2d/sketch.d.ts +1 -0
  58. package/lib/dist/features/2d/sketch.js +15 -0
  59. package/lib/dist/features/2d/vline.d.ts +9 -4
  60. package/lib/dist/features/2d/vline.js +67 -15
  61. package/lib/dist/features/common.js +2 -1
  62. package/lib/dist/features/extrude-base.d.ts +19 -1
  63. package/lib/dist/features/extrude-base.js +75 -12
  64. package/lib/dist/features/extrude-two-distances.js +32 -27
  65. package/lib/dist/features/extrude.d.ts +39 -0
  66. package/lib/dist/features/extrude.js +196 -156
  67. package/lib/dist/features/fuse.js +2 -1
  68. package/lib/dist/features/lazy-scene-object.d.ts +1 -0
  69. package/lib/dist/features/lazy-scene-object.js +3 -0
  70. package/lib/dist/features/lazy-vertex.d.ts +1 -0
  71. package/lib/dist/features/lazy-vertex.js +3 -0
  72. package/lib/dist/features/loft.js +11 -8
  73. package/lib/dist/features/mirror-shape.d.ts +2 -0
  74. package/lib/dist/features/mirror-shape.js +16 -0
  75. package/lib/dist/features/mirror-shape2d.d.ts +2 -0
  76. package/lib/dist/features/mirror-shape2d.js +22 -1
  77. package/lib/dist/features/revolve.d.ts +31 -0
  78. package/lib/dist/features/revolve.js +178 -95
  79. package/lib/dist/features/rotate.d.ts +2 -0
  80. package/lib/dist/features/rotate.js +16 -0
  81. package/lib/dist/features/rotate2d.d.ts +2 -0
  82. package/lib/dist/features/rotate2d.js +16 -0
  83. package/lib/dist/features/select.js +2 -1
  84. package/lib/dist/features/simple-extruder.d.ts +3 -1
  85. package/lib/dist/features/simple-extruder.js +13 -9
  86. package/lib/dist/features/subtract.d.ts +2 -2
  87. package/lib/dist/features/subtract.js +3 -3
  88. package/lib/dist/features/sweep.d.ts +14 -0
  89. package/lib/dist/features/sweep.js +93 -80
  90. package/lib/dist/features/translate.d.ts +2 -0
  91. package/lib/dist/features/translate.js +23 -2
  92. package/lib/dist/filters/edge/edge-filter.d.ts +6 -0
  93. package/lib/dist/filters/edge/edge-filter.js +11 -0
  94. package/lib/dist/filters/face/face-filter.d.ts +6 -0
  95. package/lib/dist/filters/face/face-filter.js +11 -0
  96. package/lib/dist/filters/filter-base.d.ts +7 -1
  97. package/lib/dist/filters/filter-base.js +8 -0
  98. package/lib/dist/filters/filter-builder-base.js +11 -0
  99. package/lib/dist/filters/from-object.d.ts +14 -0
  100. package/lib/dist/filters/from-object.js +40 -0
  101. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  102. package/lib/dist/helpers/scene-helpers.js +68 -48
  103. package/lib/dist/oc/color-transfer.js +6 -0
  104. package/lib/dist/oc/edge-ops.d.ts +1 -0
  105. package/lib/dist/oc/edge-ops.js +17 -0
  106. package/lib/dist/oc/extrude-ops.d.ts +18 -1
  107. package/lib/dist/oc/extrude-ops.js +34 -1
  108. package/lib/dist/oc/geometry.d.ts +1 -0
  109. package/lib/dist/oc/geometry.js +27 -0
  110. package/lib/dist/oc/mesh.js +11 -9
  111. package/lib/dist/oc/ray-intersect.d.ts +16 -0
  112. package/lib/dist/oc/ray-intersect.js +91 -0
  113. package/lib/dist/oc/thin-face-maker.d.ts +0 -1
  114. package/lib/dist/oc/thin-face-maker.js +2 -20
  115. package/lib/dist/rendering/render.d.ts +2 -1
  116. package/lib/dist/rendering/render.js +72 -33
  117. package/lib/dist/rendering/scene.d.ts +4 -0
  118. package/lib/dist/tests/features/2d/back.test.d.ts +1 -0
  119. package/lib/dist/tests/features/2d/back.test.js +60 -0
  120. package/lib/dist/tests/features/2d/circle.test.js +1 -1
  121. package/lib/dist/tests/features/2d/constrained.test.js +4 -4
  122. package/lib/dist/tests/features/2d/ellipse.test.d.ts +1 -0
  123. package/lib/dist/tests/features/2d/ellipse.test.js +100 -0
  124. package/lib/dist/tests/features/2d/line.test.js +89 -3
  125. package/lib/dist/tests/features/2d/offset.test.js +1 -1
  126. package/lib/dist/tests/features/2d/polygon.test.js +2 -2
  127. package/lib/dist/tests/features/2d/rect.test.js +1 -1
  128. package/lib/dist/tests/features/2d/slot-from-edge.test.js +1 -1
  129. package/lib/dist/tests/features/2d/slot.test.js +1 -1
  130. package/lib/dist/tests/features/mirror.test.js +58 -0
  131. package/lib/dist/tests/features/mirror2d.test.js +63 -0
  132. package/lib/dist/tests/features/rotate.test.js +62 -0
  133. package/lib/dist/tests/features/rotate2d.test.js +47 -0
  134. package/lib/dist/tests/features/thin-revolve.test.js +37 -1
  135. package/lib/dist/tests/features/translate.test.js +63 -0
  136. package/lib/dist/tests/perf/record-fusion-history.bench.test.d.ts +1 -0
  137. package/lib/dist/tests/perf/record-fusion-history.bench.test.js +77 -0
  138. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  139. package/package.json +1 -1
  140. package/server/dist/index.js +77 -45
  141. package/server/dist/ws-protocol.d.ts +11 -0
  142. package/ui/dist/assets/{index-BrW_x4uc.js → index-6Ep4GPxf.js} +131 -77
  143. package/ui/dist/assets/index-DRKfe6N9.css +2 -0
  144. package/ui/dist/index.html +2 -2
  145. package/ui/dist/assets/index-gPoNOiIs.css +0 -2
@@ -4,6 +4,7 @@ import { AxisObjectBase } from "../features/axis-renderable-base.js";
4
4
  import { Sketch } from "../features/2d/sketch.js";
5
5
  import { transformMeshes } from "./mesh-transform.js";
6
6
  import { ShapeOps } from "../oc/shape-ops.js";
7
+ import { Profiler } from "../common/profiler.js";
7
8
  export class SceneRenderer {
8
9
  meshBuilder = new MeshBuilder();
9
10
  render(scene) {
@@ -11,6 +12,7 @@ export class SceneRenderer {
11
12
  console.log("============ Rendering ==============", sceneObjects.length);
12
13
  const skippedContainers = new Set();
13
14
  const buildDurations = new Map();
15
+ const profilers = new Map();
14
16
  for (const object of sceneObjects) {
15
17
  // Skip descendants of cloned sketches — their edges are already
16
18
  // computed by the parent sketch's clone-mode build.
@@ -21,7 +23,9 @@ export class SceneRenderer {
21
23
  }
22
24
  console.log("Rendering object:", object.getUniqueType());
23
25
  if (!scene.isCached(object)) {
24
- buildDurations.set(object, this.buildObject(object, scene));
26
+ const result = this.buildObject(object, scene);
27
+ buildDurations.set(object, result.totalMs);
28
+ profilers.set(object, result.profiler);
25
29
  }
26
30
  // After building, mark cloned sketches so their children are skipped —
27
31
  // the sketch's build() already populated them with transformed shapes.
@@ -35,9 +39,20 @@ export class SceneRenderer {
35
39
  }
36
40
  object.clean(scene.getPartScopedAllObjects(object));
37
41
  }
42
+ const prepared = new Map();
43
+ for (const object of sceneObjects) {
44
+ const profiler = profilers.get(object);
45
+ const start = performance.now();
46
+ prepared.set(object, this.prepareRenderedShapes(object, profiler));
47
+ const meshMs = performance.now() - start;
48
+ const existing = buildDurations.get(object);
49
+ if (existing !== undefined) {
50
+ buildDurations.set(object, existing + meshMs);
51
+ }
52
+ }
38
53
  this.aggregateContainerDurations(sceneObjects, scene, buildDurations);
39
54
  for (const object of sceneObjects) {
40
- this.renderObject(object, scene, buildDurations.get(object));
55
+ this.emitRenderObject(object, scene, prepared.get(object) ?? { renderedSceneShapes: [], ownShapeCount: 0 }, buildDurations.get(object), profilers.get(object));
41
56
  }
42
57
  return scene;
43
58
  }
@@ -50,7 +65,7 @@ export class SceneRenderer {
50
65
  }
51
66
  scene.clearRenderedObjects();
52
67
  for (const obj of allObjects) {
53
- if (!scope.has(obj)) {
68
+ if (!scope.has(obj) || obj.isLazy()) {
54
69
  this.emitRendered(obj, scene, {
55
70
  sceneShapes: [],
56
71
  visible: false,
@@ -59,7 +74,7 @@ export class SceneRenderer {
59
74
  });
60
75
  continue;
61
76
  }
62
- const sceneShapes = obj.getOwnShapes({ excludeMeta: false }, scope);
77
+ const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false }, scope);
63
78
  const renderedSceneShapes = sceneShapes.map(s => this.toRenderedShape(s));
64
79
  this.emitRendered(obj, scene, {
65
80
  sceneShapes: renderedSceneShapes,
@@ -72,40 +87,53 @@ export class SceneRenderer {
72
87
  console.table(result);
73
88
  return scene;
74
89
  }
75
- renderObject(obj, scene, buildDurationMs) {
76
- const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false });
90
+ prepareRenderedShapes(obj, profiler) {
77
91
  const renderedSceneShapes = [];
92
+ if (obj.isLazy()) {
93
+ return { renderedSceneShapes, ownShapeCount: 0 };
94
+ }
78
95
  try {
96
+ const sceneShapes = obj.getOwnShapes({ excludeMeta: false, excludeGuide: false });
79
97
  if (sceneShapes.length) {
80
98
  console.log(` - Scene shapes: ${sceneShapes.length}`);
81
99
  for (const shape of sceneShapes) {
82
- renderedSceneShapes.push(this.toRenderedShape(shape));
100
+ renderedSceneShapes.push(this.toRenderedShape(shape, profiler));
83
101
  }
84
102
  }
85
- const errorMessage = obj.getError();
86
- this.emitRendered(obj, scene, {
87
- sceneShapes: renderedSceneShapes,
88
- visible: this.computeVisibility(obj, scene, sceneShapes.length),
89
- hasError: !!errorMessage,
90
- errorMessage: errorMessage || undefined,
91
- buildDurationMs,
92
- });
103
+ return { renderedSceneShapes, ownShapeCount: sceneShapes.length };
93
104
  }
94
105
  catch (error) {
95
106
  const message = error instanceof Error ? error.message : String(error);
96
107
  console.error(`Error rendering object ${obj.getUniqueType()}:`, error);
108
+ return { renderedSceneShapes, ownShapeCount: renderedSceneShapes.length, prepError: message };
109
+ }
110
+ }
111
+ emitRenderObject(obj, scene, prepared, buildDurationMs, profiler) {
112
+ if (prepared.prepError) {
97
113
  this.emitRendered(obj, scene, {
98
- sceneShapes: renderedSceneShapes,
114
+ sceneShapes: prepared.renderedSceneShapes,
99
115
  visible: false,
100
116
  hasError: true,
101
- errorMessage: message,
117
+ errorMessage: prepared.prepError,
102
118
  buildDurationMs,
119
+ profiler,
103
120
  });
121
+ return;
104
122
  }
123
+ const errorMessage = obj.getError();
124
+ this.emitRendered(obj, scene, {
125
+ sceneShapes: prepared.renderedSceneShapes,
126
+ visible: this.computeVisibility(obj, scene, prepared.ownShapeCount),
127
+ hasError: !!errorMessage,
128
+ errorMessage: errorMessage || undefined,
129
+ buildDurationMs,
130
+ profiler,
131
+ });
105
132
  }
106
133
  buildObject(object, scene) {
107
134
  object.clearError();
108
135
  const start = performance.now();
136
+ const profiler = new Profiler();
109
137
  try {
110
138
  object.build({
111
139
  getSceneObjects: () => scene.getPartScopedObjectsUpTo(object),
@@ -122,6 +150,7 @@ export class SceneRenderer {
122
150
  }
123
151
  return null;
124
152
  },
153
+ getProfiler: () => profiler,
125
154
  });
126
155
  const appliedTransform = object.getAppliedTransform();
127
156
  if (appliedTransform && !object.isContainer()) {
@@ -136,33 +165,40 @@ export class SceneRenderer {
136
165
  console.error(`Error building object ${object.getUniqueType()}:`, error);
137
166
  object.setError(message);
138
167
  }
139
- return performance.now() - start;
168
+ const totalMs = performance.now() - start;
169
+ return { totalMs, profiler };
140
170
  }
141
- getOrBuildMeshes(shape) {
171
+ getOrBuildMeshes(shape, profiler) {
142
172
  const existing = shape.getMeshes();
143
173
  if (existing) {
144
174
  return existing;
145
175
  }
146
- let meshes;
147
- const meshSource = shape.getMeshSource();
148
- if (meshSource) {
149
- let sourceMeshes = meshSource.shape.getMeshes();
150
- if (!sourceMeshes) {
151
- sourceMeshes = this.meshBuilder.build(meshSource.shape);
152
- meshSource.shape.setMeshes(sourceMeshes);
176
+ profiler?.start("Triangulation");
177
+ try {
178
+ let meshes;
179
+ const meshSource = shape.getMeshSource();
180
+ if (meshSource) {
181
+ let sourceMeshes = meshSource.shape.getMeshes();
182
+ if (!sourceMeshes) {
183
+ sourceMeshes = this.meshBuilder.build(meshSource.shape);
184
+ meshSource.shape.setMeshes(sourceMeshes);
185
+ }
186
+ meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : this.meshBuilder.build(shape);
187
+ }
188
+ else {
189
+ meshes = this.meshBuilder.build(shape);
153
190
  }
154
- meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : this.meshBuilder.build(shape);
191
+ shape.setMeshes(meshes);
192
+ return meshes;
155
193
  }
156
- else {
157
- meshes = this.meshBuilder.build(shape);
194
+ finally {
195
+ profiler?.end("Triangulation");
158
196
  }
159
- shape.setMeshes(meshes);
160
- return meshes;
161
197
  }
162
- toRenderedShape(shape) {
198
+ toRenderedShape(shape, profiler) {
163
199
  return {
164
200
  shapeId: shape.id,
165
- meshes: this.getOrBuildMeshes(shape),
201
+ meshes: this.getOrBuildMeshes(shape, profiler),
166
202
  shapeType: shape.getType(),
167
203
  isMetaShape: shape.isMetaShape() || undefined,
168
204
  isGuide: shape.isGuideShape() || undefined,
@@ -209,6 +245,8 @@ export class SceneRenderer {
209
245
  }
210
246
  }
211
247
  emitRendered(obj, scene, opts) {
248
+ const categories = opts.profiler?.getCategories();
249
+ const profileCategories = categories && categories.length > 0 ? categories : undefined;
212
250
  const rendered = {
213
251
  id: obj.id,
214
252
  name: obj.getName(),
@@ -224,6 +262,7 @@ export class SceneRenderer {
224
262
  errorMessage: opts.errorMessage,
225
263
  sourceLocation: obj.getSourceLocation() || undefined,
226
264
  buildDurationMs: opts.buildDurationMs,
265
+ profileCategories,
227
266
  };
228
267
  scene.addRenderedObject(obj, rendered);
229
268
  }
@@ -40,6 +40,10 @@ export type SceneObjectRender = {
40
40
  column: number;
41
41
  };
42
42
  buildDurationMs?: number;
43
+ profileCategories?: {
44
+ category: string;
45
+ durationMs: number;
46
+ }[];
43
47
  };
44
48
  export declare class Scene {
45
49
  private sceneObjects;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { setupOC, render } from "../../setup.js";
3
+ import sketch from "../../../core/sketch.js";
4
+ import extrude from "../../../core/extrude.js";
5
+ import { move, hMove, back, rect } from "../../../core/2d/index.js";
6
+ import { ShapeOps } from "../../../oc/shape-ops.js";
7
+ describe("back", () => {
8
+ setupOC();
9
+ it("should revert cursor to the previous position", () => {
10
+ sketch("xy", () => {
11
+ move([30, 20]);
12
+ move([100, 100]);
13
+ back();
14
+ rect(10, 10);
15
+ });
16
+ const e = extrude(5);
17
+ render();
18
+ const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
19
+ expect(bbox.minX).toBeCloseTo(30, 0);
20
+ expect(bbox.minY).toBeCloseTo(20, 0);
21
+ });
22
+ it("should revert N positions back when given a count", () => {
23
+ sketch("xy", () => {
24
+ hMove(10);
25
+ hMove(20);
26
+ hMove(40);
27
+ back(2);
28
+ rect(5, 5);
29
+ });
30
+ const e = extrude(5);
31
+ render();
32
+ const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
33
+ expect(bbox.minX).toBeCloseTo(10, 0);
34
+ expect(bbox.minY).toBeCloseTo(0, 0);
35
+ });
36
+ it("should fall back to sketch start point when count exceeds history", () => {
37
+ sketch("xy", () => {
38
+ hMove(50);
39
+ back(99);
40
+ rect(10, 10);
41
+ });
42
+ const e = extrude(5);
43
+ render();
44
+ const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
45
+ expect(bbox.minX).toBeCloseTo(0, 0);
46
+ expect(bbox.minY).toBeCloseTo(0, 0);
47
+ });
48
+ it("should toggle on consecutive back() calls", () => {
49
+ sketch("xy", () => {
50
+ hMove(40);
51
+ back();
52
+ back();
53
+ rect(10, 10);
54
+ });
55
+ const e = extrude(5);
56
+ render();
57
+ const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
58
+ expect(bbox.minX).toBeCloseTo(40, 0);
59
+ });
60
+ });
@@ -53,7 +53,7 @@ describe("circle", () => {
53
53
  });
54
54
  describe("standalone with targetPlane", () => {
55
55
  it("should create a circle on a specific plane", () => {
56
- circle(60, "xy");
56
+ circle("xy", 60);
57
57
  const e = extrude(10);
58
58
  render();
59
59
  const solid = e.getShapes()[0];
@@ -89,7 +89,7 @@ describe("constrained geometries", () => {
89
89
  describe("tCircle between circle and line", () => {
90
90
  it("should create a tangent circle to a circle and a line", () => {
91
91
  const s = sketch("xy", () => {
92
- const l = aLine(150, 45);
92
+ const l = aLine(45, 150);
93
93
  const c = circle([100, 0], 60);
94
94
  tCircle(c, l, 100).guide();
95
95
  });
@@ -101,7 +101,7 @@ describe("constrained geometries", () => {
101
101
  describe("tCircle between two lines", () => {
102
102
  it("should create a tangent circle between two lines", () => {
103
103
  const s = sketch("xy", () => {
104
- const l1 = aLine(300, 45);
104
+ const l1 = aLine(45, 300);
105
105
  move([-50, 0]);
106
106
  const l2 = vLine(300);
107
107
  tCircle(l1, l2, 200, true).guide();
@@ -136,7 +136,7 @@ describe("constrained geometries", () => {
136
136
  describe("tArc between circle and line", () => {
137
137
  it("should create a tangent arc to a circle and a line", () => {
138
138
  const s = sketch("xy", () => {
139
- const l = aLine(150, 45);
139
+ const l = aLine(45, 150);
140
140
  const c = circle([100, 0], 40);
141
141
  tArc(c, l, 50).guide();
142
142
  });
@@ -148,7 +148,7 @@ describe("constrained geometries", () => {
148
148
  describe("tArc between two lines", () => {
149
149
  it("should create a fillet arc between two lines", () => {
150
150
  const s = sketch("xy", () => {
151
- const l1 = aLine(150, 45);
151
+ const l1 = aLine(45, 150);
152
152
  move([-50, 0]);
153
153
  const l2 = vLine(100);
154
154
  tArc(l1, l2, 50).guide();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { setupOC, render } from "../../setup.js";
3
+ import sketch from "../../../core/sketch.js";
4
+ import extrude from "../../../core/extrude.js";
5
+ import { ellipse } from "../../../core/2d/index.js";
6
+ import { ShapeOps } from "../../../oc/shape-ops.js";
7
+ describe("ellipse", () => {
8
+ setupOC();
9
+ describe("in sketch", () => {
10
+ it("creates an ellipse with rx along X and ry along Y", () => {
11
+ sketch("xy", () => {
12
+ ellipse(50, 30);
13
+ });
14
+ const e = extrude(10);
15
+ render();
16
+ const solid = e.getShapes()[0];
17
+ const bbox = ShapeOps.getBoundingBox(solid);
18
+ expect(bbox.maxX - bbox.minX).toBeCloseTo(100, 0);
19
+ expect(bbox.maxY - bbox.minY).toBeCloseTo(60, 0);
20
+ });
21
+ it("handles ry > rx (axis-swap path)", () => {
22
+ sketch("xy", () => {
23
+ ellipse(30, 50);
24
+ });
25
+ const e = extrude(10);
26
+ render();
27
+ const solid = e.getShapes()[0];
28
+ const bbox = ShapeOps.getBoundingBox(solid);
29
+ expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
30
+ expect(bbox.maxY - bbox.minY).toBeCloseTo(100, 0);
31
+ });
32
+ it("creates an ellipse at a given center", () => {
33
+ sketch("xy", () => {
34
+ ellipse([50, 30], 40, 20);
35
+ });
36
+ const e = extrude(10);
37
+ render();
38
+ const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
39
+ expect(bbox.centerX).toBeCloseTo(50, 0);
40
+ expect(bbox.centerY).toBeCloseTo(30, 0);
41
+ expect(bbox.maxX - bbox.minX).toBeCloseTo(80, 0);
42
+ expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
43
+ });
44
+ it("falls through to a circle when rx == ry", () => {
45
+ sketch("xy", () => {
46
+ ellipse(25, 25);
47
+ });
48
+ const e = extrude(10);
49
+ render();
50
+ const solid = e.getShapes()[0];
51
+ const bbox = ShapeOps.getBoundingBox(solid);
52
+ expect(bbox.maxX - bbox.minX).toBeCloseTo(50, 0);
53
+ expect(bbox.maxY - bbox.minY).toBeCloseTo(50, 0);
54
+ });
55
+ it("rejects zero or negative radii", () => {
56
+ let zeroEllipse;
57
+ sketch("xy", () => {
58
+ zeroEllipse = ellipse(0, 30);
59
+ });
60
+ render();
61
+ expect(zeroEllipse?.getError()).toMatch(/positive/i);
62
+ let negEllipse;
63
+ sketch("xy", () => {
64
+ negEllipse = ellipse(-10, 5);
65
+ });
66
+ render();
67
+ expect(negEllipse?.getError()).toMatch(/positive/i);
68
+ });
69
+ it("throws when given a plane inside a sketch", () => {
70
+ expect(() => {
71
+ sketch("xy", () => {
72
+ ellipse("xy", 30, 20);
73
+ });
74
+ render();
75
+ }).toThrow();
76
+ });
77
+ });
78
+ describe("standalone with targetPlane", () => {
79
+ it("creates an ellipse on a specific plane", () => {
80
+ ellipse("xy", 60, 40);
81
+ const e = extrude(10);
82
+ render();
83
+ const solid = e.getShapes()[0];
84
+ const bbox = ShapeOps.getBoundingBox(solid);
85
+ expect(bbox.maxX - bbox.minX).toBeCloseTo(120, 0);
86
+ expect(bbox.maxY - bbox.minY).toBeCloseTo(80, 0);
87
+ });
88
+ it("creates an ellipse on a plane at a given center", () => {
89
+ ellipse("xy", [10, 20], 30, 15);
90
+ const e = extrude(10);
91
+ render();
92
+ const solid = e.getShapes()[0];
93
+ const bbox = ShapeOps.getBoundingBox(solid);
94
+ expect(bbox.centerX).toBeCloseTo(10, 0);
95
+ expect(bbox.centerY).toBeCloseTo(20, 0);
96
+ expect(bbox.maxX - bbox.minX).toBeCloseTo(60, 0);
97
+ expect(bbox.maxY - bbox.minY).toBeCloseTo(30, 0);
98
+ });
99
+ });
100
+ });
@@ -2,8 +2,9 @@ import { describe, it, expect } from "vitest";
2
2
  import { setupOC, render } from "../../setup.js";
3
3
  import sketch from "../../../core/sketch.js";
4
4
  import extrude from "../../../core/extrude.js";
5
- import { line, hLine, vLine, aLine } from "../../../core/2d/index.js";
5
+ import { line, hLine, vLine, aLine, circle, move } from "../../../core/2d/index.js";
6
6
  import { ShapeOps } from "../../../oc/shape-ops.js";
7
+ import { Edge } from "../../../common/edge.js";
7
8
  describe("line functions", () => {
8
9
  setupOC();
9
10
  describe("line", () => {
@@ -32,7 +33,7 @@ describe("line functions", () => {
32
33
  expect(bbox.maxY - bbox.minY).toBeCloseTo(40, 0);
33
34
  });
34
35
  it("should support standalone mode with targetPlane", () => {
35
- hLine(50, "xy");
36
+ hLine("xy", 50);
36
37
  render();
37
38
  // Just verify no error — standalone line doesn't form a closed shape
38
39
  });
@@ -57,7 +58,7 @@ describe("line functions", () => {
57
58
  it("should create an angled line", () => {
58
59
  sketch("xy", () => {
59
60
  hLine(50);
60
- aLine(50, 90);
61
+ aLine(90, 50);
61
62
  hLine(-50);
62
63
  vLine(-50);
63
64
  });
@@ -66,6 +67,91 @@ describe("line functions", () => {
66
67
  expect(e.getShapes()).toHaveLength(1);
67
68
  });
68
69
  });
70
+ describe("hLine to target geometry", () => {
71
+ it("should end at the nearest intersection with a circle", () => {
72
+ let h;
73
+ sketch("xy", () => {
74
+ const c = circle([100, 0], 50);
75
+ h = hLine([0, 0], c);
76
+ });
77
+ render();
78
+ const edges = h.getOwnShapes().filter((sh) => sh instanceof Edge);
79
+ expect(edges).toHaveLength(1);
80
+ const endPoint = edges[0].getLastVertex().toPoint();
81
+ // Circle at (100, 0) with diameter 50 → radius 25 → near edge at x=75
82
+ expect(endPoint.x).toBeCloseTo(75, 1);
83
+ expect(endPoint.y).toBeCloseTo(0, 1);
84
+ });
85
+ it("should pick nearest intersection when target is behind the start", () => {
86
+ let h;
87
+ sketch("xy", () => {
88
+ const c = circle([-100, 0], 50);
89
+ h = hLine([0, 0], c);
90
+ });
91
+ render();
92
+ const edges = h.getOwnShapes().filter((sh) => sh instanceof Edge);
93
+ const endPoint = edges[0].getLastVertex().toPoint();
94
+ // Circle at (-100, 0) with diameter 50 → near edge at x=-75
95
+ expect(endPoint.x).toBeCloseTo(-75, 1);
96
+ expect(endPoint.y).toBeCloseTo(0, 1);
97
+ });
98
+ it("should record an error when there is no intersection", () => {
99
+ let h;
100
+ sketch("xy", () => {
101
+ const c = circle([0, 100], 20);
102
+ h = hLine([0, 0], c);
103
+ });
104
+ render();
105
+ expect(h.getError()).toMatch(/does not intersect/);
106
+ });
107
+ it("should record an error when .centered() is combined with a target", () => {
108
+ let h;
109
+ sketch("xy", () => {
110
+ const c = circle([100, 0], 40);
111
+ h = hLine([0, 0], c).centered();
112
+ });
113
+ render();
114
+ expect(h.getError()).toMatch(/centered/);
115
+ });
116
+ });
117
+ describe("vLine to target geometry", () => {
118
+ it("should end at the nearest intersection with a circle above", () => {
119
+ let v;
120
+ sketch("xy", () => {
121
+ const c = circle([0, 100], 50);
122
+ v = vLine([0, 0], c);
123
+ });
124
+ render();
125
+ const edges = v.getOwnShapes().filter((sh) => sh instanceof Edge);
126
+ const endPoint = edges[0].getLastVertex().toPoint();
127
+ expect(endPoint.x).toBeCloseTo(0, 1);
128
+ expect(endPoint.y).toBeCloseTo(75, 1);
129
+ });
130
+ });
131
+ describe("aLine to target geometry", () => {
132
+ it("should end where the angled line meets a horizontal line", () => {
133
+ let a;
134
+ sketch("xy", () => {
135
+ // A horizontal segment at y = 50 (drawn as guide for intersection).
136
+ // Use hLine starting at (-100, 50) with length 200 so it spans x ∈ [-100, 100].
137
+ const h = hLine([-100, 50], 200);
138
+ // Now place a 45° line starting at the origin; previous tangent is (1,0)
139
+ // (left over from h's hLine). Rotated 45° CCW that's direction (√2/2, √2/2).
140
+ // Starting from (100, 50)? Actually current position after h is (100, 50).
141
+ // We want aLine at angle 45° from current direction (1,0) rotated by 45°
142
+ // → direction (√2/2, √2/2). Starting from (100, 50), going at 45° → never
143
+ // hits the segment again. Use move() to reset start.
144
+ move([0, 0]);
145
+ a = aLine(45, h);
146
+ });
147
+ render();
148
+ const edges = a.getOwnShapes().filter((sh) => sh instanceof Edge);
149
+ const endPoint = edges[0].getLastVertex().toPoint();
150
+ // 45° line from (0,0) hits y=50 at x=50.
151
+ expect(endPoint.x).toBeCloseTo(50, 1);
152
+ expect(endPoint.y).toBeCloseTo(50, 1);
153
+ });
154
+ });
69
155
  describe("combined line functions", () => {
70
156
  it("should create an L-shape with hLine and vLine", () => {
71
157
  sketch("xy", () => {
@@ -91,7 +91,7 @@ describe("offset", () => {
91
91
  const s = sketch("xy", () => {
92
92
  circle(40);
93
93
  });
94
- offset(5, false, "xy", s);
94
+ offset("xy", 5, false, s);
95
95
  render();
96
96
  // Offset edges should exist on the target plane
97
97
  });
@@ -133,14 +133,14 @@ describe("polygon", () => {
133
133
  });
134
134
  describe("standalone with targetPlane", () => {
135
135
  it("should create a polygon on a specific plane", () => {
136
- polygon(5, 50, "xy");
136
+ polygon("xy", 5, 50);
137
137
  const e = extrude(10);
138
138
  render();
139
139
  const solid = e.getShapes()[0];
140
140
  expect(solid.getFaces()).toHaveLength(7);
141
141
  });
142
142
  it("should create a circumscribed polygon on a specific plane", () => {
143
- polygon(6, 60, "circumscribed", "xy");
143
+ polygon("xy", 6, 60, "circumscribed");
144
144
  const e = extrude(10);
145
145
  render();
146
146
  const solid = e.getShapes()[0];
@@ -54,7 +54,7 @@ describe("rect", () => {
54
54
  });
55
55
  describe("standalone with targetPlane", () => {
56
56
  it("should create a rectangle on a specific plane", () => {
57
- rect(80, 40, "xy");
57
+ rect("xy", 80, 40);
58
58
  const e = extrude(10);
59
59
  render();
60
60
  const solid = e.getShapes()[0];
@@ -15,7 +15,7 @@ describe("slot from edge", () => {
15
15
  });
16
16
  it("should create a slot from an angled line", () => {
17
17
  const s = sketch("xy", () => {
18
- const l = aLine(60, 45);
18
+ const l = aLine(45, 60);
19
19
  slot(l, 10);
20
20
  });
21
21
  render();
@@ -35,7 +35,7 @@ describe("slot", () => {
35
35
  });
36
36
  describe("standalone with targetPlane", () => {
37
37
  it("should create a slot on a specific plane", () => {
38
- slot(60, 10, "xy");
38
+ slot("xy", 60, 10);
39
39
  const e = extrude(10);
40
40
  render();
41
41
  expect(e.getShapes()).toHaveLength(1);