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
@@ -109,4 +109,62 @@ describe("mirror (3D)", () => {
109
109
  expect(countShapes(scene)).toBe(1);
110
110
  });
111
111
  });
112
+ describe("mirror with .exclude()", () => {
113
+ it("should skip excluded objects when mirroring everything", () => {
114
+ sketch("xy", () => {
115
+ move([20, 0]);
116
+ rect(30, 30);
117
+ });
118
+ const e1 = extrude(10).new();
119
+ sketch("xy", () => {
120
+ move([50, 50]);
121
+ rect(30, 30);
122
+ });
123
+ extrude(10).new();
124
+ // mirror all, but exclude e1 → only e2 gets mirrored
125
+ // e1 + e2 + mirror(e2) = 3 shapes (e2 is well clear of YZ plane, no fusion)
126
+ mirror("yz").exclude(e1);
127
+ const scene = render();
128
+ expect(countShapes(scene)).toBe(3);
129
+ });
130
+ it("should narrow an explicit target list with exclude", () => {
131
+ sketch("xy", () => {
132
+ move([20, 0]);
133
+ rect(30, 30);
134
+ });
135
+ const e1 = extrude(10).new();
136
+ sketch("xy", () => {
137
+ move([50, 50]);
138
+ rect(30, 30);
139
+ });
140
+ const e2 = extrude(10).new();
141
+ // explicit target [e1, e2], exclude e2 → only e1 mirrored
142
+ // e1 + e2 + mirror(e1) = 3
143
+ mirror("yz", e1, e2).exclude(e2);
144
+ const scene = render();
145
+ expect(countShapes(scene)).toBe(3);
146
+ });
147
+ it("should accumulate exclusions across chained calls", () => {
148
+ sketch("xy", () => {
149
+ move([20, 0]);
150
+ rect(30, 30);
151
+ });
152
+ const e1 = extrude(10).new();
153
+ sketch("xy", () => {
154
+ move([50, 50]);
155
+ rect(30, 30);
156
+ });
157
+ const e2 = extrude(10).new();
158
+ sketch("xy", () => {
159
+ move([100, 100]);
160
+ rect(30, 30);
161
+ });
162
+ extrude(10).new();
163
+ // mirror everything but exclude e1 and e2 across two chained .exclude() calls
164
+ // e1 + e2 + e3 + mirror(e3) = 4 (all on +X side, mirror lands on -X side)
165
+ mirror("yz").exclude(e1).exclude(e2);
166
+ const scene = render();
167
+ expect(countShapes(scene)).toBe(4);
168
+ });
169
+ });
112
170
  });
@@ -106,4 +106,67 @@ describe("mirror (2D)", () => {
106
106
  expect(() => local("y")).toThrow();
107
107
  });
108
108
  });
109
+ describe("mirror with .exclude()", () => {
110
+ it("should skip an excluded geometry when mirroring all sketch siblings", () => {
111
+ sketch("xy", () => {
112
+ move([30, 0]);
113
+ const c1 = circle(10);
114
+ move([30, 50]);
115
+ const c2 = circle(10);
116
+ // mirror everything in the sketch but skip c2
117
+ mirror("y").exclude(c2);
118
+ // sanity: keep references referenced
119
+ void c1;
120
+ });
121
+ const e = extrude(5);
122
+ render();
123
+ const shapes = e.getShapes();
124
+ expect(shapes.length).toBeGreaterThanOrEqual(3);
125
+ const bboxes = shapes.map(s => ShapeOps.getBoundingBox(s));
126
+ // the c2 mirror would have minX < -10 AND minY > 40 — should NOT exist
127
+ const c2MirrorPresent = bboxes.some(b => b.minX < -10 && b.minY > 40);
128
+ expect(c2MirrorPresent).toBe(false);
129
+ });
130
+ it("should narrow an explicit target list with exclude", () => {
131
+ sketch("xy", () => {
132
+ move([30, 0]);
133
+ const c1 = circle(10);
134
+ move([30, 50]);
135
+ const c2 = circle(10);
136
+ // explicit targets [c1, c2], then exclude c2 → only c1 is mirrored
137
+ mirror("y", c1, c2).exclude(c2);
138
+ });
139
+ const e = extrude(5);
140
+ render();
141
+ const shapes = e.getShapes();
142
+ const bboxes = shapes.map(s => ShapeOps.getBoundingBox(s));
143
+ // c2 mirror at (-30, 50) should be absent
144
+ const c2MirrorPresent = bboxes.some(b => b.minX < -10 && b.minY > 40);
145
+ expect(c2MirrorPresent).toBe(false);
146
+ // c1 mirror at (-30, 0) should be present
147
+ const c1MirrorPresent = bboxes.some(b => b.minX < -10 && b.maxY < 40);
148
+ expect(c1MirrorPresent).toBe(true);
149
+ });
150
+ it("should accumulate exclusions across chained calls", () => {
151
+ sketch("xy", () => {
152
+ move([30, 0]);
153
+ const c1 = circle(10);
154
+ move([30, 50]);
155
+ const c2 = circle(10);
156
+ move([30, 100]);
157
+ const c3 = circle(10);
158
+ // exclude c1 and c2 in two calls; only c3 should be mirrored
159
+ mirror("y").exclude(c1).exclude(c2);
160
+ void c3;
161
+ });
162
+ const e = extrude(5);
163
+ render();
164
+ const shapes = e.getShapes();
165
+ const bboxes = shapes.map(s => ShapeOps.getBoundingBox(s));
166
+ // c1 mirror (-30, 0) absent
167
+ expect(bboxes.some(b => b.minX < -10 && b.maxY < 40)).toBe(false);
168
+ // c3 mirror (-30, 100) present
169
+ expect(bboxes.some(b => b.minX < -10 && b.minY > 90)).toBe(true);
170
+ });
171
+ });
109
172
  });
@@ -130,4 +130,66 @@ describe("rotate", () => {
130
130
  expect(bbox.maxY).toBeCloseTo(0, 0);
131
131
  });
132
132
  });
133
+ describe("rotate with .exclude()", () => {
134
+ it("should skip excluded objects when rotating everything", () => {
135
+ sketch("xy", () => {
136
+ rect(20, 10);
137
+ });
138
+ const e1 = extrude(5).new();
139
+ sketch("xy", () => {
140
+ rect(20, 10);
141
+ });
142
+ const e2 = extrude(5).new();
143
+ // No explicit target → rotate all, exclude e1 → only e2 rotates
144
+ rotate("z", 90, true).exclude(e1);
145
+ render();
146
+ // e1 still at (0..20, 0..10) — unrotated
147
+ expect(e1.getShapes()).toHaveLength(1);
148
+ const e1Bbox = ShapeOps.getBoundingBox(e1.getShapes()[0]);
149
+ expect(e1Bbox.maxX).toBeCloseTo(20, 0);
150
+ expect(e1Bbox.maxY).toBeCloseTo(10, 0);
151
+ // e2 still present (copy=true)
152
+ expect(e2.getShapes()).toHaveLength(1);
153
+ });
154
+ it("should narrow an explicit target list with exclude", () => {
155
+ sketch("xy", () => {
156
+ rect(20, 10);
157
+ });
158
+ const e1 = extrude(5).new();
159
+ sketch("xy", () => {
160
+ rect(20, 10);
161
+ });
162
+ const e2 = extrude(5).new();
163
+ // Explicit targets [e1, e2], exclude e2 → only e1 rotated
164
+ rotate("z", 90, e1, e2).exclude(e2);
165
+ render();
166
+ // e1 moved (removed from source), e2 stays
167
+ expect(e1.getShapes()).toHaveLength(0);
168
+ expect(e2.getShapes()).toHaveLength(1);
169
+ const e2Bbox = ShapeOps.getBoundingBox(e2.getShapes()[0]);
170
+ expect(e2Bbox.maxX).toBeCloseTo(20, 0);
171
+ expect(e2Bbox.maxY).toBeCloseTo(10, 0);
172
+ });
173
+ it("should accumulate exclusions across chained calls", () => {
174
+ sketch("xy", () => {
175
+ rect(20, 10);
176
+ });
177
+ const e1 = extrude(5).new();
178
+ sketch("xy", () => {
179
+ rect(20, 10);
180
+ });
181
+ const e2 = extrude(5).new();
182
+ sketch("xy", () => {
183
+ rect(20, 10);
184
+ });
185
+ const e3 = extrude(5).new();
186
+ // Rotate all (copy), exclude e1 and e2 across two calls → only e3 rotated copy
187
+ rotate("z", 90, true).exclude(e1).exclude(e2);
188
+ render();
189
+ // e1 and e2 unchanged at (0..20, 0..10)
190
+ expect(ShapeOps.getBoundingBox(e1.getShapes()[0]).maxY).toBeCloseTo(10, 0);
191
+ expect(ShapeOps.getBoundingBox(e2.getShapes()[0]).maxY).toBeCloseTo(10, 0);
192
+ expect(e3.getShapes()).toHaveLength(1);
193
+ });
194
+ });
133
195
  });
@@ -71,4 +71,51 @@ describe("rotate 2D", () => {
71
71
  expect(shapes.length).toBeGreaterThanOrEqual(1);
72
72
  });
73
73
  });
74
+ describe("rotate (2D) with .exclude()", () => {
75
+ it("should skip an excluded geometry when rotating all sketch siblings", () => {
76
+ let rot;
77
+ sketch("xy", () => {
78
+ move([30, 0]);
79
+ const r1 = rect(20, 10);
80
+ move([30, 50]);
81
+ const r2 = rect(20, 10);
82
+ rot = rotate(90, true).exclude(r2);
83
+ void r1;
84
+ });
85
+ render();
86
+ // Only r1 should have been rotated (r2 excluded)
87
+ // 1 rotated rect = 4 edges
88
+ expect(rot.getAddedShapes()).toHaveLength(4);
89
+ });
90
+ it("should narrow an explicit target list with exclude", () => {
91
+ let rot;
92
+ sketch("xy", () => {
93
+ move([30, 0]);
94
+ const r1 = rect(20, 10);
95
+ move([30, 50]);
96
+ const r2 = rect(20, 10);
97
+ rot = rotate(90, true, r1, r2).exclude(r2);
98
+ });
99
+ render();
100
+ // 1 rotated rect = 4 edges
101
+ expect(rot.getAddedShapes()).toHaveLength(4);
102
+ });
103
+ it("should accumulate exclusions across chained calls", () => {
104
+ let rot;
105
+ sketch("xy", () => {
106
+ move([30, 0]);
107
+ const r1 = rect(20, 10);
108
+ move([30, 50]);
109
+ const r2 = rect(20, 10);
110
+ move([30, 100]);
111
+ const r3 = rect(20, 10);
112
+ rot = rotate(90, true).exclude(r1).exclude(r2);
113
+ void r3;
114
+ });
115
+ render();
116
+ // Only r3 rotated; r1 and r2 excluded
117
+ // 1 rotated rect = 4 edges
118
+ expect(rot.getAddedShapes()).toHaveLength(4);
119
+ });
120
+ });
74
121
  });
@@ -3,7 +3,7 @@ import { setupOC, render } from "../setup.js";
3
3
  import sketch from "../../core/sketch.js";
4
4
  import revolve from "../../core/revolve.js";
5
5
  import extrude from "../../core/extrude.js";
6
- import { move, rect, circle, line } from "../../core/2d/index.js";
6
+ import { move, rect, circle, line, vLine } from "../../core/2d/index.js";
7
7
  import { ShapeProps } from "../../oc/props.js";
8
8
  import { EdgeQuery } from "../../oc/edge-query.js";
9
9
  describe("thin revolve", () => {
@@ -133,6 +133,42 @@ describe("thin revolve", () => {
133
133
  expect(internalFaces.length).toBeGreaterThan(0);
134
134
  expect(capFaces.length).toBe(2);
135
135
  });
136
+ it("should classify cap faces for vertical line profile parallel to axis (90 deg)", () => {
137
+ sketch("xy", () => {
138
+ move([50, 0]);
139
+ vLine(100).centered();
140
+ });
141
+ const r = revolve("y", -90).thin(20).new();
142
+ render();
143
+ const startFaces = r.getState('start-faces');
144
+ const endFaces = r.getState('end-faces');
145
+ const sideFaces = r.getState('side-faces');
146
+ const internalFaces = r.getState('internal-faces');
147
+ const capFaces = r.getState('cap-faces');
148
+ expect(startFaces.length).toBe(1);
149
+ expect(endFaces.length).toBe(1);
150
+ expect(sideFaces.length).toBe(1);
151
+ expect(internalFaces.length).toBe(1);
152
+ expect(capFaces.length).toBe(2);
153
+ });
154
+ it("should classify cap faces for vertical line profile parallel to axis (180 deg)", () => {
155
+ sketch("xy", () => {
156
+ move([50, 0]);
157
+ vLine(100).centered();
158
+ });
159
+ const r = revolve("y", 180).thin(20).new();
160
+ render();
161
+ const startFaces = r.getState('start-faces');
162
+ const endFaces = r.getState('end-faces');
163
+ const sideFaces = r.getState('side-faces');
164
+ const internalFaces = r.getState('internal-faces');
165
+ const capFaces = r.getState('cap-faces');
166
+ expect(startFaces.length).toBe(1);
167
+ expect(endFaces.length).toBe(1);
168
+ expect(sideFaces.length).toBe(1);
169
+ expect(internalFaces.length).toBe(1);
170
+ expect(capFaces.length).toBe(2);
171
+ });
136
172
  });
137
173
  describe("remove mode", () => {
138
174
  it("should cut a thin-walled revolve from existing geometry", () => {
@@ -114,4 +114,67 @@ describe("translate", () => {
114
114
  expect(bbox.minX).toBeCloseTo(0, 0);
115
115
  });
116
116
  });
117
+ describe("translate with .exclude()", () => {
118
+ it("should skip excluded objects when translating everything", () => {
119
+ sketch("xy", () => {
120
+ rect(20, 20);
121
+ });
122
+ const e1 = extrude(10).new();
123
+ sketch("xy", () => {
124
+ rect(20, 20);
125
+ });
126
+ const e2 = extrude(10).new();
127
+ // No explicit target → translate all, exclude e1 → only e2 moves
128
+ translate(100, true).exclude(e1);
129
+ render();
130
+ // e1 stays at origin
131
+ expect(e1.getShapes()).toHaveLength(1);
132
+ const e1Bbox = ShapeOps.getBoundingBox(e1.getShapes()[0]);
133
+ expect(e1Bbox.minX).toBeCloseTo(0, 0);
134
+ // e2 was copied to x=100 (original still present because copy=true)
135
+ expect(e2.getShapes()).toHaveLength(1);
136
+ });
137
+ it("should narrow an explicit target list with exclude", () => {
138
+ sketch("xy", () => {
139
+ rect(20, 20);
140
+ });
141
+ const e1 = extrude(10).new();
142
+ sketch("xy", () => {
143
+ rect(20, 20);
144
+ });
145
+ const e2 = extrude(10).new();
146
+ // Explicit targets [e1, e2], exclude e2 → only e1 moves
147
+ translate(100, e1, e2).exclude(e2);
148
+ render();
149
+ // e1 moved (removed from source), e2 stays
150
+ expect(e1.getShapes()).toHaveLength(0);
151
+ expect(e2.getShapes()).toHaveLength(1);
152
+ const e2Bbox = ShapeOps.getBoundingBox(e2.getShapes()[0]);
153
+ expect(e2Bbox.minX).toBeCloseTo(0, 0);
154
+ });
155
+ it("should accumulate exclusions across chained calls", () => {
156
+ sketch("xy", () => {
157
+ rect(20, 20);
158
+ });
159
+ const e1 = extrude(10).new();
160
+ sketch("xy", () => {
161
+ rect(20, 20);
162
+ });
163
+ const e2 = extrude(10).new();
164
+ sketch("xy", () => {
165
+ rect(20, 20);
166
+ });
167
+ const e3 = extrude(10).new();
168
+ // Translate all (copy), exclude e1 and e2 across two calls → only e3 copied
169
+ translate(200, true).exclude(e1).exclude(e2);
170
+ render();
171
+ // All originals preserved (copy=true)
172
+ expect(e1.getShapes()).toHaveLength(1);
173
+ expect(e2.getShapes()).toHaveLength(1);
174
+ expect(e3.getShapes()).toHaveLength(1);
175
+ // e1 and e2 still at origin
176
+ expect(ShapeOps.getBoundingBox(e1.getShapes()[0]).minX).toBeCloseTo(0, 0);
177
+ expect(ShapeOps.getBoundingBox(e2.getShapes()[0]).minX).toBeCloseTo(0, 0);
178
+ });
179
+ });
117
180
  });
@@ -0,0 +1,77 @@
1
+ import { describe, it } 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 cylinder from "../../core/cylinder.js";
6
+ import { circle, rect, move } from "../../core/2d/index.js";
7
+ // Run with `LOG_PERF=1 npm test -- --run lib/tests/perf/` to see the breakdown.
8
+ const LOG = process.env.LOG_PERF === "1";
9
+ function dump(scene, label, target) {
10
+ if (!LOG)
11
+ return;
12
+ const rendered = scene.getRenderedObject(target);
13
+ const categories = rendered?.profileCategories ?? [];
14
+ console.log(`\n=== ${label} ===`);
15
+ console.log(`buildDurationMs=${rendered?.buildDurationMs?.toFixed(1)}ms`);
16
+ for (const c of categories) {
17
+ console.log(` ${c.category.padEnd(30)} ${c.durationMs.toFixed(1)}ms`);
18
+ }
19
+ }
20
+ describe("perf: record fusion history breakdown", () => {
21
+ setupOC();
22
+ it("simple cylinder + extrude fuse", () => {
23
+ cylinder(20, 20);
24
+ sketch("xy", () => { circle(8); });
25
+ const e = extrude(40);
26
+ const s = render();
27
+ dump(s, "simple cylinder + extrude(40) fuse", e);
28
+ });
29
+ it("box + extrude fuse — bigger result", () => {
30
+ sketch("xy", () => { rect(80, 80); });
31
+ extrude(40);
32
+ sketch("xy", () => { circle(15); });
33
+ const e = extrude(80);
34
+ const s = render();
35
+ dump(s, "box(80,80,40) + extrude(80) fuse", e);
36
+ });
37
+ it("complex: many small holes through a big block", () => {
38
+ sketch("xy", () => { rect(100, 100); });
39
+ extrude(40);
40
+ for (let i = 0; i < 5; i++) {
41
+ for (let j = 0; j < 5; j++) {
42
+ sketch("xy", () => { move([i * 18 - 36, j * 18 - 36]); circle(3); });
43
+ }
44
+ }
45
+ sketch("xy", () => { circle(40); });
46
+ const e = extrude(40);
47
+ const s = render();
48
+ dump(s, "block + extrude over many holes", e);
49
+ });
50
+ it("very complex: big block with N small holes (single fuse)", () => {
51
+ sketch("xy", () => { rect(200, 200); });
52
+ extrude(40);
53
+ const N = 8;
54
+ sketch("xy", () => {
55
+ for (let i = 0; i < N; i++) {
56
+ for (let j = 0; j < N; j++) {
57
+ move([i * 22 - 80, j * 22 - 80]);
58
+ circle(4);
59
+ }
60
+ }
61
+ });
62
+ const e = extrude(60);
63
+ const s = render();
64
+ dump(s, `block + single extrude with ${N * N} holes`, e);
65
+ });
66
+ it("incremental fuses: extrude many small features one-by-one", () => {
67
+ sketch("xy", () => { rect(150, 150); });
68
+ extrude(30);
69
+ let last = null;
70
+ for (let i = 0; i < 10; i++) {
71
+ sketch("xy", () => { move([i * 12 - 50, 0]); circle(4); });
72
+ last = extrude(40);
73
+ }
74
+ const s = render();
75
+ dump(s, "10th incremental extrude (after building up complexity)", last);
76
+ });
77
+ });