fluidcad 0.0.21 → 0.0.22

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 (41) hide show
  1. package/README.md +7 -2
  2. package/lib/dist/common/shape.d.ts +7 -0
  3. package/lib/dist/common/shape.js +7 -0
  4. package/lib/dist/core/copy.js +1 -1
  5. package/lib/dist/core/draft.d.ts +16 -0
  6. package/lib/dist/core/draft.js +29 -0
  7. package/lib/dist/core/index.d.ts +2 -3
  8. package/lib/dist/core/index.js +1 -1
  9. package/lib/dist/core/interfaces.d.ts +2 -0
  10. package/lib/dist/core/part.d.ts +2 -6
  11. package/lib/dist/core/part.js +12 -22
  12. package/lib/dist/features/2d/sketch.d.ts +1 -0
  13. package/lib/dist/features/2d/sketch.js +21 -4
  14. package/lib/dist/features/copy-circular.js +1 -0
  15. package/lib/dist/features/copy-circular2d.js +1 -0
  16. package/lib/dist/features/copy-linear.js +4 -5
  17. package/lib/dist/features/copy-linear2d.js +4 -5
  18. package/lib/dist/features/draft.d.ts +15 -0
  19. package/lib/dist/features/draft.js +88 -0
  20. package/lib/dist/oc/draft-ops.d.ts +5 -0
  21. package/lib/dist/oc/draft-ops.js +51 -0
  22. package/lib/dist/oc/mesh.d.ts +2 -0
  23. package/lib/dist/oc/mesh.js +14 -6
  24. package/lib/dist/rendering/mesh-transform.d.ts +3 -0
  25. package/lib/dist/rendering/mesh-transform.js +22 -0
  26. package/lib/dist/rendering/render-solid.js +3 -2
  27. package/lib/dist/rendering/render.js +25 -4
  28. package/lib/dist/tests/features/draft.test.d.ts +1 -0
  29. package/lib/dist/tests/features/draft.test.js +147 -0
  30. package/lib/dist/tests/features/part.test.js +69 -114
  31. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/server/dist/fluidcad-server.d.ts +2 -0
  34. package/server/dist/fluidcad-server.js +10 -0
  35. package/server/dist/routes/actions.js +20 -0
  36. package/server/dist/vite-manager.js +7 -1
  37. package/ui/dist/assets/{index-B1LkrBga.js → index-C0JwQ8Bk.js} +9 -5
  38. package/ui/dist/assets/{index-BfcNNxXr.css → index-gPoNOiIs.css} +1 -1
  39. package/ui/dist/index.html +2 -2
  40. package/lib/dist/core/use.d.ts +0 -5
  41. package/lib/dist/core/use.js +0 -22
@@ -2,6 +2,7 @@ import { MeshBuilder } from "./mesh-builder.js";
2
2
  import { PlaneObjectBase } from "../features/plane-renderable-base.js";
3
3
  import { AxisObjectBase } from "../features/axis-renderable-base.js";
4
4
  import { Sketch } from "../features/2d/sketch.js";
5
+ import { transformMeshes } from "./mesh-transform.js";
5
6
  const meshBuilder = new MeshBuilder();
6
7
  function renderSceneObject(obj, scene, buildDurationMs) {
7
8
  const hasError = !!obj.getError();
@@ -12,7 +13,18 @@ function renderSceneObject(obj, scene, buildDurationMs) {
12
13
  for (const shape of sceneShapes) {
13
14
  let meshes = shape.getMeshes();
14
15
  if (!meshes) {
15
- meshes = meshBuilder.build(shape);
16
+ const meshSource = shape.getMeshSource();
17
+ if (meshSource) {
18
+ let sourceMeshes = meshSource.shape.getMeshes();
19
+ if (!sourceMeshes) {
20
+ sourceMeshes = meshBuilder.build(meshSource.shape);
21
+ meshSource.shape.setMeshes(sourceMeshes);
22
+ }
23
+ meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : meshBuilder.build(shape);
24
+ }
25
+ else {
26
+ meshes = meshBuilder.build(shape);
27
+ }
16
28
  shape.setMeshes(meshes);
17
29
  }
18
30
  const shapeType = shape.getType();
@@ -71,7 +83,18 @@ export function renderSceneRollback(scene, rollbackIndex) {
71
83
  for (const shape of sceneShapes) {
72
84
  let meshes = shape.getMeshes();
73
85
  if (!meshes) {
74
- meshes = meshBuilder.build(shape);
86
+ const meshSource = shape.getMeshSource();
87
+ if (meshSource) {
88
+ let sourceMeshes = meshSource.shape.getMeshes();
89
+ if (!sourceMeshes) {
90
+ sourceMeshes = meshBuilder.build(meshSource.shape);
91
+ meshSource.shape.setMeshes(sourceMeshes);
92
+ }
93
+ meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : meshBuilder.build(shape);
94
+ }
95
+ else {
96
+ meshes = meshBuilder.build(shape);
97
+ }
75
98
  shape.setMeshes(meshes);
76
99
  }
77
100
  renderedSceneShapes.push({
@@ -219,7 +242,5 @@ export function renderScene(scene) {
219
242
  for (const object of sceneObjects) {
220
243
  renderSceneObject(object, scene, buildDurations.get(object));
221
244
  }
222
- const result = scene.getRenderedObjects();
223
- console.table(result);
224
245
  return scene;
225
246
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,147 @@
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 draft from "../../core/draft.js";
6
+ import select from "../../core/select.js";
7
+ import { rect } from "../../core/2d/index.js";
8
+ import { countShapes } from "../utils.js";
9
+ import { ShapeOps } from "../../oc/shape-ops.js";
10
+ import { ShapeProps } from "../../oc/props.js";
11
+ import { face } from "../../filters/index.js";
12
+ describe("draft", () => {
13
+ setupOC();
14
+ describe("draft with implicit selection", () => {
15
+ it("should apply draft to the last selected face", () => {
16
+ sketch("xy", () => {
17
+ rect(100, 100);
18
+ });
19
+ extrude(80);
20
+ select(face().onPlane("front"));
21
+ draft(5);
22
+ const scene = render();
23
+ expect(countShapes(scene)).toBe(1);
24
+ const solid = scene.getAllSceneObjects()
25
+ .flatMap(o => o.getShapes())
26
+ .find(s => s.getType() === "solid");
27
+ expect(solid).toBeDefined();
28
+ });
29
+ it("should change the bounding box when drafting a side face", () => {
30
+ sketch("xy", () => {
31
+ rect(100, 100);
32
+ });
33
+ extrude(80);
34
+ select(face().onPlane("front"));
35
+ draft(5);
36
+ const scene = render();
37
+ const solid = scene.getAllSceneObjects()
38
+ .flatMap(o => o.getShapes())
39
+ .find(s => s.getType() === "solid");
40
+ const bbox = ShapeOps.getBoundingBox(solid);
41
+ // 5 deg draft over 80mm height: tan(5°) * 80 ≈ 7mm extension
42
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(103);
43
+ });
44
+ it("should change volume compared to original box", () => {
45
+ sketch("xy", () => {
46
+ rect(100, 100);
47
+ });
48
+ extrude(80);
49
+ select(face().onPlane("front"));
50
+ draft(5);
51
+ const scene = render();
52
+ const solid = scene.getAllSceneObjects()
53
+ .flatMap(o => o.getShapes())
54
+ .find(s => s.getType() === "solid");
55
+ const props = ShapeProps.getProperties(solid.getShape());
56
+ const originalVolume = 100 * 100 * 80;
57
+ expect(Math.abs(props.volumeMm3 - originalVolume)).toBeGreaterThan(1000);
58
+ });
59
+ });
60
+ describe("draft with explicit selection", () => {
61
+ it("should apply draft using e.sideFaces()", () => {
62
+ sketch("xy", () => {
63
+ rect(100, 100);
64
+ });
65
+ const e = extrude(80);
66
+ draft(5, e.sideFaces());
67
+ const scene = render();
68
+ expect(countShapes(scene)).toBe(1);
69
+ const solid = scene.getAllSceneObjects()
70
+ .flatMap(o => o.getShapes())
71
+ .find(s => s.getType() === "solid");
72
+ expect(solid).toBeDefined();
73
+ });
74
+ it("should apply draft using explicit select()", () => {
75
+ sketch("xy", () => {
76
+ rect(100, 100);
77
+ });
78
+ extrude(80);
79
+ const sel = select(face().onPlane("front"));
80
+ draft(5, sel);
81
+ const scene = render();
82
+ expect(countShapes(scene)).toBe(1);
83
+ });
84
+ });
85
+ describe("draft angle effect", () => {
86
+ it("should produce larger extension with 10 degrees than 3 degrees", () => {
87
+ sketch("xy", () => {
88
+ rect(100, 100);
89
+ });
90
+ extrude(80);
91
+ select(face().onPlane("front"));
92
+ draft(10);
93
+ const scene = render();
94
+ const solid = scene.getAllSceneObjects()
95
+ .flatMap(o => o.getShapes())
96
+ .find(s => s.getType() === "solid");
97
+ const bbox = ShapeOps.getBoundingBox(solid);
98
+ // tan(10°) * 80 ≈ 14.1mm
99
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(110);
100
+ });
101
+ it("should produce smaller extension with 3 degrees", () => {
102
+ sketch("xy", () => {
103
+ rect(100, 100);
104
+ });
105
+ extrude(80);
106
+ select(face().onPlane("front"));
107
+ draft(3);
108
+ const scene = render();
109
+ const solid = scene.getAllSceneObjects()
110
+ .flatMap(o => o.getShapes())
111
+ .find(s => s.getType() === "solid");
112
+ const bbox = ShapeOps.getBoundingBox(solid);
113
+ // tan(3°) * 80 ≈ 4.2mm
114
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(103);
115
+ expect(bbox.maxY - bbox.minY).toBeLessThan(110);
116
+ });
117
+ });
118
+ describe("draft removes selection shapes", () => {
119
+ it("should remove the face selection after drafting", () => {
120
+ sketch("xy", () => {
121
+ rect(100, 100);
122
+ });
123
+ extrude(80);
124
+ const sel = select(face().onPlane("front"));
125
+ draft(5, sel);
126
+ render();
127
+ expect(sel.getShapes()).toHaveLength(0);
128
+ });
129
+ });
130
+ describe("draft on multiple faces", () => {
131
+ it("should draft all four side faces via sideFaces()", () => {
132
+ sketch("xy", () => {
133
+ rect(100, 100);
134
+ });
135
+ const e = extrude(80);
136
+ draft(5, e.sideFaces());
137
+ const scene = render();
138
+ const solid = scene.getAllSceneObjects()
139
+ .flatMap(o => o.getShapes())
140
+ .find(s => s.getType() === "solid");
141
+ const bbox = ShapeOps.getBoundingBox(solid);
142
+ // All four sides drafted — bounding box should extend in both X and Y
143
+ expect(bbox.maxX - bbox.minX).toBeGreaterThan(103);
144
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(103);
145
+ });
146
+ });
147
+ });
@@ -1,6 +1,6 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
1
+ import { describe, it, expect } from "vitest";
2
2
  import { setupOC, render } from "../setup.js";
3
- import { getCurrentScene, setCurrentFile } from "../../scene-manager.js";
3
+ import { getCurrentScene } from "../../scene-manager.js";
4
4
  import sketch from "../../core/sketch.js";
5
5
  import extrude from "../../core/extrude.js";
6
6
  import cut from "../../core/cut.js";
@@ -8,84 +8,67 @@ import repeat from "../../core/repeat.js";
8
8
  import translate from "../../core/translate.js";
9
9
  import { circle, rect } from "../../core/2d/index.js";
10
10
  import part from "../../core/part.js";
11
- import use from "../../core/use.js";
12
11
  import { Part } from "../../features/part.js";
13
12
  import { Extrude } from "../../features/extrude.js";
14
13
  import { Sketch } from "../../features/2d/sketch.js";
15
14
  import { countShapes } from "../utils.js";
16
15
  describe("part", () => {
17
16
  setupOC();
18
- beforeEach(() => {
19
- // Simulate direct editing: currentFile matches the source location
20
- // Since tests don't have .fluid.js stack frames, we set currentFile to empty
21
- // so that direct execution tests need special handling
22
- setCurrentFile('');
23
- });
24
- describe("use()", () => {
25
- it("should execute a part handle via use()", () => {
26
- const handle = part("my-part", () => {
27
- sketch("xy", () => {
28
- circle(10);
29
- });
30
- extrude(20);
17
+ it("should execute a part callback immediately", () => {
18
+ part("my-part", () => {
19
+ sketch("xy", () => {
20
+ circle(10);
31
21
  });
32
- // Since there's no .fluid.js source frame, the callback won't auto-execute.
33
- // We need use() to trigger it.
34
- use(handle);
35
- const scene = getCurrentScene();
36
- const objects = scene.getAllSceneObjects();
37
- // Should have Part, Plane, Sketch, Circle, Extrude
38
- const parts = objects.filter(o => o instanceof Part);
39
- expect(parts).toHaveLength(1);
40
- expect(parts[0].partName).toBe("my-part");
22
+ extrude(20);
41
23
  });
42
- it("should produce a solid when used", () => {
43
- const handle = part("solid-part", () => {
44
- sketch("xy", () => {
45
- circle(10);
46
- });
47
- extrude(20);
24
+ const scene = getCurrentScene();
25
+ const objects = scene.getAllSceneObjects();
26
+ const parts = objects.filter(o => o instanceof Part);
27
+ expect(parts).toHaveLength(1);
28
+ expect(parts[0].partName).toBe("my-part");
29
+ });
30
+ it("should produce a solid", () => {
31
+ part("solid-part", () => {
32
+ sketch("xy", () => {
33
+ circle(10);
48
34
  });
49
- use(handle);
50
- render();
51
- const scene = getCurrentScene();
52
- const objects = scene.getAllSceneObjects();
53
- const extrudeObj = objects.find(o => o instanceof Extrude);
54
- expect(extrudeObj).toBeDefined();
55
- const shapes = extrudeObj.getShapes();
56
- expect(shapes.length).toBeGreaterThan(0);
57
- expect(shapes[0].getType()).toBe("solid");
35
+ extrude(20);
58
36
  });
37
+ render();
38
+ const scene = getCurrentScene();
39
+ const objects = scene.getAllSceneObjects();
40
+ const extrudeObj = objects.find(o => o instanceof Extrude);
41
+ expect(extrudeObj).toBeDefined();
42
+ const shapes = extrudeObj.getShapes();
43
+ expect(shapes.length).toBeGreaterThan(0);
44
+ expect(shapes[0].getType()).toBe("solid");
59
45
  });
60
46
  describe("fusion isolation", () => {
61
47
  it("should keep two parts as separate solids", () => {
62
- const handle1 = part("part1", () => {
48
+ part("part1", () => {
63
49
  sketch("xy", () => {
64
50
  circle(10);
65
51
  });
66
52
  extrude(20);
67
53
  });
68
- const handle2 = part("part2", () => {
54
+ part("part2", () => {
69
55
  sketch("xy", () => {
70
56
  circle(10);
71
57
  });
72
58
  extrude(20);
73
59
  });
74
- use(handle1);
75
- use(handle2);
76
60
  render();
77
61
  const scene = getCurrentScene();
78
62
  const objects = scene.getAllSceneObjects();
79
63
  const extrudes = objects.filter(o => o instanceof Extrude);
80
64
  expect(extrudes).toHaveLength(2);
81
- // Each extrude should have its own solid — they should NOT be fused together
82
65
  const shapes1 = extrudes[0].getShapes();
83
66
  const shapes2 = extrudes[1].getShapes();
84
67
  expect(shapes1.length).toBeGreaterThan(0);
85
68
  expect(shapes2.length).toBeGreaterThan(0);
86
69
  });
87
70
  it("should fuse extrudes within the same part", () => {
88
- const handle = part("fusing-part", () => {
71
+ part("fusing-part", () => {
89
72
  sketch("xy", () => {
90
73
  circle([-5, 0], 10);
91
74
  });
@@ -95,14 +78,11 @@ describe("part", () => {
95
78
  });
96
79
  extrude(20);
97
80
  });
98
- use(handle);
99
81
  render();
100
82
  const scene = getCurrentScene();
101
83
  const objects = scene.getAllSceneObjects();
102
84
  const extrudes = objects.filter(o => o instanceof Extrude);
103
85
  expect(extrudes).toHaveLength(2);
104
- // The second extrude should have fused with the first (overlapping circles)
105
- // So the first extrude's shapes should have been removed (consumed by fusion)
106
86
  const shapes1 = extrudes[0].getShapes();
107
87
  const shapes2 = extrudes[1].getShapes();
108
88
  expect(shapes1).toHaveLength(0);
@@ -111,13 +91,12 @@ describe("part", () => {
111
91
  });
112
92
  describe("container structure", () => {
113
93
  it("should make sketch a child of the part", () => {
114
- const handle = part("container-test", () => {
94
+ part("container-test", () => {
115
95
  sketch("xy", () => {
116
96
  circle(10);
117
97
  });
118
98
  extrude(20);
119
99
  });
120
- use(handle);
121
100
  const scene = getCurrentScene();
122
101
  const objects = scene.getAllSceneObjects();
123
102
  const partObj = objects.find(o => o instanceof Part);
@@ -153,41 +132,35 @@ describe("part", () => {
153
132
  });
154
133
  const e2 = extrude(20);
155
134
  render();
156
- // Second extrude should fuse with first (overlapping)
157
135
  const shapes = e2.getShapes();
158
136
  expect(shapes.length).toBeGreaterThan(0);
159
137
  });
160
138
  });
161
139
  describe("repeat inside part", () => {
162
140
  it("should repeat with explicit objects", () => {
163
- const handle = part("repeat-explicit", () => {
141
+ part("repeat-explicit", () => {
164
142
  sketch("xy", () => {
165
143
  rect(50);
166
144
  });
167
145
  const e = extrude().new();
168
146
  repeat("linear", "x", { count: 3, offset: 80 }, e);
169
147
  });
170
- use(handle);
171
148
  const scene = render();
172
- // Original + 2 clones = 3 shapes
173
149
  expect(countShapes(scene)).toBe(3);
174
150
  });
175
151
  it("should repeat with default input (last object)", () => {
176
- const handle = part("repeat-default", () => {
152
+ part("repeat-default", () => {
177
153
  sketch("xy", () => {
178
154
  rect(50);
179
155
  });
180
156
  extrude().new();
181
157
  repeat("linear", "x", { count: 3, offset: 80 });
182
158
  });
183
- use(handle);
184
159
  const scene = render();
185
160
  expect(countShapes(scene)).toBe(3);
186
161
  });
187
162
  it("should not override clone parent-child relationships", () => {
188
- // Regression: cloned Sketch children (Rect) must keep Sketch as parent,
189
- // not be reparented to the Part container
190
- const handle = part("clone-parents", () => {
163
+ part("clone-parents", () => {
191
164
  sketch("xy", () => {
192
165
  rect(50);
193
166
  });
@@ -198,22 +171,19 @@ describe("part", () => {
198
171
  const c = cut();
199
172
  repeat("linear", "x", { count: 3, offset: 80 }, e, c);
200
173
  });
201
- use(handle);
202
174
  const scene = render();
203
- // Should produce 3 complete instances (original + 2 clones)
204
175
  expect(countShapes(scene)).toBe(3);
205
176
  });
206
177
  });
207
178
  describe("pick inside part", () => {
208
179
  it("should preserve pick meta shapes on extrude inside a part", () => {
209
- const handle = part("pick-test", () => {
180
+ part("pick-test", () => {
210
181
  sketch("xy", () => {
211
182
  rect(50);
212
183
  circle(20);
213
184
  });
214
185
  extrude().pick();
215
186
  });
216
- use(handle);
217
187
  const scene = render();
218
188
  const rendered = scene.getRenderedObjects();
219
189
  const extrudeRender = rendered.find(r => r.type === 'extrude');
@@ -222,78 +192,71 @@ describe("part", () => {
222
192
  expect(metaShapes.length).toBeGreaterThan(0);
223
193
  });
224
194
  it("should preserve pick meta shapes with multiple parts", () => {
225
- const handle1 = part("pick-part1", () => {
195
+ part("pick-part1", () => {
226
196
  sketch("xy", () => {
227
197
  rect(50);
228
198
  circle(20);
229
199
  });
230
200
  extrude().pick();
231
201
  });
232
- const handle2 = part("pick-part2", () => {
202
+ part("pick-part2", () => {
233
203
  sketch("xy", () => {
234
204
  circle(10);
235
205
  });
236
206
  extrude();
237
207
  });
238
- use(handle1);
239
- use(handle2);
240
208
  const scene = render();
241
209
  const rendered = scene.getRenderedObjects();
242
210
  const extrudeRenders = rendered.filter(r => r.type === 'extrude');
243
- // First extrude (pick) should still have meta shapes
244
211
  const metaShapes = extrudeRenders[0].sceneShapes.filter(s => s.isMetaShape);
245
212
  expect(metaShapes.length).toBeGreaterThan(0);
246
213
  });
247
214
  });
248
- describe("use() as transform target", () => {
215
+ describe("part() as transform target", () => {
249
216
  it("should return an ISceneObject (Part instance)", () => {
250
- const handle = part("ret-test", () => {
217
+ const result = part("ret-test", () => {
251
218
  sketch("xy", () => { circle(10); });
252
219
  extrude(20);
253
220
  });
254
- const result = use(handle);
255
221
  expect(result).toBeDefined();
256
222
  expect(result).toBeInstanceOf(Part);
257
223
  });
258
224
  it("should translate a Part target", () => {
259
- const handle = part("translate-part", () => {
225
+ const p = part("translate-part", () => {
260
226
  sketch("xy", () => { rect(20, 20); });
261
227
  extrude(10);
262
228
  });
263
- const p = use(handle);
264
229
  translate(50, 0, 0, p);
265
230
  const scene = render();
266
231
  expect(countShapes(scene)).toBe(1);
267
232
  });
268
233
  it("should translate-copy a Part target", () => {
269
- const handle = part("copy-part", () => {
234
+ const p = part("copy-part", () => {
270
235
  sketch("xy", () => { rect(20, 20); });
271
236
  extrude(10);
272
237
  });
273
- const p = use(handle);
274
238
  translate(50, 0, 0, true, p);
275
239
  const scene = render();
276
- // Original + copy = 2 shapes
277
240
  expect(countShapes(scene)).toBe(2);
278
241
  });
279
242
  it("should transform only the targeted Part", () => {
280
- const handle = part("multi", (options) => {
281
- sketch("xy", () => { rect(options.size, options.size); });
282
- extrude(options.size);
243
+ const p1 = part("multi-a", () => {
244
+ sketch("xy", () => { rect(10, 10); });
245
+ extrude(10);
246
+ });
247
+ part("multi-b", () => {
248
+ sketch("xy", () => { rect(20, 20); });
249
+ extrude(20);
283
250
  });
284
- const p1 = use(handle, { size: 10 });
285
- const p2 = use(handle, { size: 20 });
286
251
  translate(100, 0, 0, p1);
287
252
  const scene = render();
288
- // Both parts should still produce shapes (2 total)
289
253
  expect(countShapes(scene)).toBe(2);
290
254
  });
291
255
  it("should work inline with translate", () => {
292
- const handle = part("inline-part", () => {
256
+ translate(50, 0, 0, part("inline-part", () => {
293
257
  sketch("xy", () => { circle(10); });
294
258
  extrude(20);
295
- });
296
- translate(50, 0, 0, use(handle));
259
+ }));
297
260
  const scene = render();
298
261
  expect(countShapes(scene)).toBe(1);
299
262
  });
@@ -304,15 +267,17 @@ describe("part", () => {
304
267
  expect(p.isAlwaysVisible()).toBe(true);
305
268
  });
306
269
  });
307
- describe("options", () => {
308
- it("should pass options from use() to the part callback", () => {
309
- const handle = part("opts-part", (options) => {
310
- sketch("xy", () => {
311
- circle(options.radius);
270
+ describe("parameterized parts via closures", () => {
271
+ it("should support parameterization through closures", () => {
272
+ function makePart(radius, height) {
273
+ return part("opts-part", () => {
274
+ sketch("xy", () => {
275
+ circle(radius);
276
+ });
277
+ extrude(height);
312
278
  });
313
- extrude(options.height);
314
- });
315
- use(handle, { radius: 15, height: 30 });
279
+ }
280
+ makePart(15, 30);
316
281
  render();
317
282
  const scene = getCurrentScene();
318
283
  const objects = scene.getAllSceneObjects();
@@ -325,15 +290,17 @@ describe("part", () => {
325
290
  expect(shapes.length).toBeGreaterThan(0);
326
291
  expect(shapes[0].getType()).toBe("solid");
327
292
  });
328
- it("should allow reusing a part with different options", () => {
329
- const handle = part("reusable", (options) => {
330
- sketch("xy", () => {
331
- circle(options.size);
293
+ it("should create separate parts with different parameters", () => {
294
+ function makePart(name, size) {
295
+ return part(name, () => {
296
+ sketch("xy", () => {
297
+ circle(size);
298
+ });
299
+ extrude(size);
332
300
  });
333
- extrude(options.size);
334
- });
335
- use(handle, { size: 10 });
336
- use(handle, { size: 20 });
301
+ }
302
+ makePart("reusable-a", 10);
303
+ makePart("reusable-b", 20);
337
304
  render();
338
305
  const scene = getCurrentScene();
339
306
  const objects = scene.getAllSceneObjects();
@@ -341,16 +308,4 @@ describe("part", () => {
341
308
  expect(parts).toHaveLength(2);
342
309
  });
343
310
  });
344
- describe("PartHandle", () => {
345
- it("should have correct properties", () => {
346
- const handle = part("test-handle", () => { });
347
- expect(handle.__fluidcad_part).toBe(true);
348
- expect(handle.name).toBe("test-handle");
349
- expect(handle._callback).toBeInstanceOf(Function);
350
- });
351
- it("should throw when use() is given invalid input", () => {
352
- expect(() => use(null)).toThrow("use() expects a PartHandle");
353
- expect(() => use({})).toThrow("use() expects a PartHandle");
354
- });
355
- });
356
311
  });