fluidcad 0.0.26 → 0.0.28

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 (93) hide show
  1. package/lib/dist/common/scene-object.d.ts +45 -0
  2. package/lib/dist/common/scene-object.js +121 -0
  3. package/lib/dist/common/shape-factory.d.ts +1 -1
  4. package/lib/dist/common/shape-history-tracker.d.ts +35 -0
  5. package/lib/dist/common/shape-history-tracker.js +114 -0
  6. package/lib/dist/common/shape.js +7 -1
  7. package/lib/dist/common/shapes.d.ts +0 -1
  8. package/lib/dist/common/shapes.js +0 -1
  9. package/lib/dist/common/solid.js +5 -1
  10. package/lib/dist/core/extrude.d.ts +12 -13
  11. package/lib/dist/core/extrude.js +19 -1
  12. package/lib/dist/core/part.d.ts +2 -1
  13. package/lib/dist/core/part.js +4 -1
  14. package/lib/dist/core/sketch.d.ts +4 -3
  15. package/lib/dist/core/sketch.js +4 -1
  16. package/lib/dist/features/chamfer.js +12 -6
  17. package/lib/dist/features/extrude-base.d.ts +43 -1
  18. package/lib/dist/features/extrude-base.js +141 -36
  19. package/lib/dist/features/extrude-to-face.d.ts +1 -1
  20. package/lib/dist/features/extrude-to-face.js +42 -19
  21. package/lib/dist/features/extrude-two-distances.d.ts +1 -1
  22. package/lib/dist/features/extrude-two-distances.js +41 -15
  23. package/lib/dist/features/extrude.d.ts +1 -1
  24. package/lib/dist/features/extrude.js +75 -20
  25. package/lib/dist/features/fillet.js +3 -4
  26. package/lib/dist/features/fuse.js +14 -0
  27. package/lib/dist/features/infinite-extrude.d.ts +1 -0
  28. package/lib/dist/features/infinite-extrude.js +33 -4
  29. package/lib/dist/features/loft.js +18 -5
  30. package/lib/dist/features/mirror-shape.d.ts +1 -3
  31. package/lib/dist/features/mirror-shape.js +2 -1
  32. package/lib/dist/features/revolve.js +17 -4
  33. package/lib/dist/features/rotate.js +1 -0
  34. package/lib/dist/features/simple-extruder.js +5 -0
  35. package/lib/dist/features/sweep.js +13 -2
  36. package/lib/dist/features/translate.js +3 -1
  37. package/lib/dist/filters/face/face-filter.d.ts +12 -0
  38. package/lib/dist/filters/face/face-filter.js +21 -0
  39. package/lib/dist/filters/face/torus-filter.d.ts +19 -0
  40. package/lib/dist/filters/face/torus-filter.js +38 -0
  41. package/lib/dist/helpers/scene-helpers.d.ts +10 -2
  42. package/lib/dist/helpers/scene-helpers.js +278 -10
  43. package/lib/dist/index.d.ts +1 -0
  44. package/lib/dist/oc/boolean-ops.d.ts +32 -4
  45. package/lib/dist/oc/boolean-ops.js +122 -11
  46. package/lib/dist/oc/color-transfer.d.ts +37 -0
  47. package/lib/dist/oc/color-transfer.js +135 -0
  48. package/lib/dist/oc/extrude-ops.js +25 -3
  49. package/lib/dist/oc/face-ops.d.ts +0 -1
  50. package/lib/dist/oc/face-ops.js +0 -13
  51. package/lib/dist/oc/face-query.d.ts +2 -0
  52. package/lib/dist/oc/face-query.js +30 -0
  53. package/lib/dist/oc/fillet-ops.d.ts +5 -3
  54. package/lib/dist/oc/fillet-ops.js +107 -70
  55. package/lib/dist/oc/intersection.js +6 -3
  56. package/lib/dist/oc/mesh.d.ts +25 -2
  57. package/lib/dist/oc/mesh.js +112 -35
  58. package/lib/dist/oc/shape-ops.d.ts +25 -20
  59. package/lib/dist/oc/shape-ops.js +129 -113
  60. package/lib/dist/rendering/mesh-transform.js +17 -1
  61. package/lib/dist/rendering/render-solid.js +19 -6
  62. package/lib/dist/rendering/render-wire.js +2 -0
  63. package/lib/dist/rendering/render.d.ts +12 -2
  64. package/lib/dist/rendering/render.js +195 -220
  65. package/lib/dist/scene-manager.d.ts +2 -0
  66. package/lib/dist/scene-manager.js +4 -3
  67. package/lib/dist/tests/common/scene-object-history.test.d.ts +1 -0
  68. package/lib/dist/tests/common/scene-object-history.test.js +274 -0
  69. package/lib/dist/tests/common/shape-history-tracker.test.d.ts +1 -0
  70. package/lib/dist/tests/common/shape-history-tracker.test.js +110 -0
  71. package/lib/dist/tests/features/2d/project-regression.test.d.ts +1 -0
  72. package/lib/dist/tests/features/2d/project-regression.test.js +69 -0
  73. package/lib/dist/tests/features/2d/project-user-regression.test.d.ts +1 -0
  74. package/lib/dist/tests/features/2d/project-user-regression.test.js +37 -0
  75. package/lib/dist/tests/features/color-lineage.test.d.ts +1 -0
  76. package/lib/dist/tests/features/color-lineage.test.js +213 -0
  77. package/lib/dist/tests/features/cut-symmetric-through-all.test.d.ts +1 -0
  78. package/lib/dist/tests/features/cut-symmetric-through-all.test.js +32 -0
  79. package/lib/dist/tests/features/extrude-history.test.d.ts +1 -0
  80. package/lib/dist/tests/features/extrude-history.test.js +248 -0
  81. package/lib/dist/tests/features/extrude.test.js +71 -0
  82. package/lib/dist/tests/features/fillet2d.test.js +16 -1
  83. package/lib/dist/tests/features/peer-ops-history.test.d.ts +1 -0
  84. package/lib/dist/tests/features/peer-ops-history.test.js +119 -0
  85. package/lib/dist/tests/features/select.test.js +50 -0
  86. package/lib/dist/tests/features/subtract.test.js +21 -1
  87. package/lib/dist/tests/setup.js +3 -2
  88. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +3 -3
  90. package/ui/dist/assets/{index-BeLxRMCv.js → index-BrW_x4uc.js} +37 -37
  91. package/ui/dist/index.html +1 -1
  92. package/lib/dist/common/solid-face.d.ts +0 -9
  93. package/lib/dist/common/solid-face.js +0 -22
@@ -0,0 +1,274 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { SceneObject } from "../../common/scene-object.js";
3
+ import { Face } from "../../common/face.js";
4
+ import { Edge } from "../../common/edge.js";
5
+ class FakeSceneObject extends SceneObject {
6
+ _container;
7
+ constructor(options = {}) {
8
+ super();
9
+ this._container = options.container ?? false;
10
+ }
11
+ isContainer() {
12
+ return this._container;
13
+ }
14
+ getType() {
15
+ return "fake";
16
+ }
17
+ serialize() {
18
+ return {};
19
+ }
20
+ build() { }
21
+ }
22
+ function makeFace() {
23
+ return new Face(null);
24
+ }
25
+ function makeEdge() {
26
+ return new Edge(null);
27
+ }
28
+ describe("SceneObject history tracking", () => {
29
+ describe("defaults", () => {
30
+ it("returns empty arrays for every new getter on a fresh object", () => {
31
+ const obj = new FakeSceneObject();
32
+ expect(obj.getAddedFaces()).toEqual([]);
33
+ expect(obj.getModifiedFaces()).toEqual([]);
34
+ expect(obj.getRemovedFaces()).toEqual([]);
35
+ expect(obj.getAddedEdges()).toEqual([]);
36
+ expect(obj.getModifiedEdges()).toEqual([]);
37
+ expect(obj.getRemovedEdges()).toEqual([]);
38
+ expect(obj.getFinalShapes()).toEqual([]);
39
+ });
40
+ });
41
+ describe("face additions", () => {
42
+ it("records an added face and preserves addedBy", () => {
43
+ const owner = new FakeSceneObject();
44
+ const by = new FakeSceneObject();
45
+ const face = makeFace();
46
+ owner.recordAddedFace(face, by);
47
+ expect(owner.getAddedFaces()).toEqual([face]);
48
+ });
49
+ it("scopes addition lookups by addedBy", () => {
50
+ const owner = new FakeSceneObject();
51
+ const a = new FakeSceneObject();
52
+ const b = new FakeSceneObject();
53
+ const fa = makeFace();
54
+ const fb = makeFace();
55
+ owner.recordAddedFace(fa, a);
56
+ owner.recordAddedFace(fb, b);
57
+ expect(owner.getAddedFaces()).toEqual([fa, fb]);
58
+ expect(owner.getAddedFaces(new Set([a]))).toEqual([fa]);
59
+ expect(owner.getAddedFaces(new Set([b]))).toEqual([fb]);
60
+ expect(owner.getAddedFaces(new Set([a, b]))).toEqual([fa, fb]);
61
+ });
62
+ });
63
+ describe("face modifications", () => {
64
+ it("records a 1:1 modification with length-1 source and result arrays", () => {
65
+ const owner = new FakeSceneObject();
66
+ const by = new FakeSceneObject();
67
+ const src = makeFace();
68
+ const dst = makeFace();
69
+ owner.recordModifiedFaces([src], [dst], by);
70
+ const records = owner.getModifiedFaces();
71
+ expect(records).toHaveLength(1);
72
+ expect(records[0].sources).toEqual([src]);
73
+ expect(records[0].results).toEqual([dst]);
74
+ expect(records[0].modifiedBy).toBe(by);
75
+ });
76
+ it("records a 1:N split as one record with multiple results", () => {
77
+ const owner = new FakeSceneObject();
78
+ const by = new FakeSceneObject();
79
+ const src = makeFace();
80
+ const r1 = makeFace();
81
+ const r2 = makeFace();
82
+ const r3 = makeFace();
83
+ owner.recordModifiedFaces([src], [r1, r2, r3], by);
84
+ const records = owner.getModifiedFaces();
85
+ expect(records).toHaveLength(1);
86
+ expect(records[0].sources).toEqual([src]);
87
+ expect(records[0].results).toEqual([r1, r2, r3]);
88
+ });
89
+ it("records an N:1 merge (UnifySameDomain shape) as one record with multiple sources", () => {
90
+ const owner = new FakeSceneObject();
91
+ const by = new FakeSceneObject();
92
+ const s1 = makeFace();
93
+ const s2 = makeFace();
94
+ const s3 = makeFace();
95
+ const merged = makeFace();
96
+ owner.recordModifiedFaces([s1, s2, s3], [merged], by);
97
+ const records = owner.getModifiedFaces();
98
+ expect(records).toHaveLength(1);
99
+ expect(records[0].sources).toEqual([s1, s2, s3]);
100
+ expect(records[0].results).toEqual([merged]);
101
+ });
102
+ it("scopes modification lookups by modifiedBy", () => {
103
+ const owner = new FakeSceneObject();
104
+ const a = new FakeSceneObject();
105
+ const b = new FakeSceneObject();
106
+ const fa = makeFace();
107
+ const fb = makeFace();
108
+ owner.recordModifiedFaces([fa], [makeFace()], a);
109
+ owner.recordModifiedFaces([fb], [makeFace()], b);
110
+ expect(owner.getModifiedFaces()).toHaveLength(2);
111
+ expect(owner.getModifiedFaces(new Set([a]))).toHaveLength(1);
112
+ expect(owner.getModifiedFaces(new Set([a]))[0].sources).toEqual([fa]);
113
+ });
114
+ });
115
+ describe("face removals", () => {
116
+ it("records a removed face with removedBy", () => {
117
+ const owner = new FakeSceneObject();
118
+ const by = new FakeSceneObject();
119
+ const face = makeFace();
120
+ owner.recordRemovedFace(face, by);
121
+ expect(owner.getRemovedFaces()).toEqual([face]);
122
+ });
123
+ it("scopes removal lookups by removedBy", () => {
124
+ const owner = new FakeSceneObject();
125
+ const a = new FakeSceneObject();
126
+ const b = new FakeSceneObject();
127
+ const fa = makeFace();
128
+ const fb = makeFace();
129
+ owner.recordRemovedFace(fa, a);
130
+ owner.recordRemovedFace(fb, b);
131
+ expect(owner.getRemovedFaces(new Set([a]))).toEqual([fa]);
132
+ expect(owner.getRemovedFaces(new Set([b]))).toEqual([fb]);
133
+ });
134
+ it("propagates to the owning child when called on a container", () => {
135
+ const container = new FakeSceneObject({ container: true });
136
+ const child1 = new FakeSceneObject();
137
+ const child2 = new FakeSceneObject();
138
+ container.addChildObject(child1);
139
+ container.addChildObject(child2);
140
+ const by = new FakeSceneObject();
141
+ const face = makeFace();
142
+ child1.recordAddedFace(face, child1);
143
+ container.recordRemovedFace(face, by);
144
+ expect(child1.getRemovedFaces()).toEqual([face]);
145
+ expect(child2.getRemovedFaces()).toEqual([]);
146
+ // The container itself must not store the record directly.
147
+ expect(container.getRemovedFaces()).toEqual([]);
148
+ });
149
+ it("does not propagate to children that do not own the face", () => {
150
+ const container = new FakeSceneObject({ container: true });
151
+ const child = new FakeSceneObject();
152
+ container.addChildObject(child);
153
+ const by = new FakeSceneObject();
154
+ const unowned = makeFace();
155
+ container.recordRemovedFace(unowned, by);
156
+ expect(child.getRemovedFaces()).toEqual([]);
157
+ });
158
+ });
159
+ describe("edge additions", () => {
160
+ it("records an added edge and preserves addedBy", () => {
161
+ const owner = new FakeSceneObject();
162
+ const by = new FakeSceneObject();
163
+ const edge = makeEdge();
164
+ owner.recordAddedEdge(edge, by);
165
+ expect(owner.getAddedEdges()).toEqual([edge]);
166
+ });
167
+ it("scopes addition lookups by addedBy", () => {
168
+ const owner = new FakeSceneObject();
169
+ const a = new FakeSceneObject();
170
+ const b = new FakeSceneObject();
171
+ const ea = makeEdge();
172
+ const eb = makeEdge();
173
+ owner.recordAddedEdge(ea, a);
174
+ owner.recordAddedEdge(eb, b);
175
+ expect(owner.getAddedEdges(new Set([a]))).toEqual([ea]);
176
+ expect(owner.getAddedEdges(new Set([b]))).toEqual([eb]);
177
+ });
178
+ });
179
+ describe("edge modifications", () => {
180
+ it("records a 1:1 edge modification", () => {
181
+ const owner = new FakeSceneObject();
182
+ const by = new FakeSceneObject();
183
+ const src = makeEdge();
184
+ const dst = makeEdge();
185
+ owner.recordModifiedEdges([src], [dst], by);
186
+ const records = owner.getModifiedEdges();
187
+ expect(records).toHaveLength(1);
188
+ expect(records[0].sources).toEqual([src]);
189
+ expect(records[0].results).toEqual([dst]);
190
+ expect(records[0].modifiedBy).toBe(by);
191
+ });
192
+ it("records a 1:N edge split", () => {
193
+ const owner = new FakeSceneObject();
194
+ const by = new FakeSceneObject();
195
+ const src = makeEdge();
196
+ const r1 = makeEdge();
197
+ const r2 = makeEdge();
198
+ owner.recordModifiedEdges([src], [r1, r2], by);
199
+ expect(owner.getModifiedEdges()).toHaveLength(1);
200
+ expect(owner.getModifiedEdges()[0].results).toEqual([r1, r2]);
201
+ });
202
+ it("records an N:1 edge merge", () => {
203
+ const owner = new FakeSceneObject();
204
+ const by = new FakeSceneObject();
205
+ const s1 = makeEdge();
206
+ const s2 = makeEdge();
207
+ const merged = makeEdge();
208
+ owner.recordModifiedEdges([s1, s2], [merged], by);
209
+ expect(owner.getModifiedEdges()).toHaveLength(1);
210
+ expect(owner.getModifiedEdges()[0].sources).toEqual([s1, s2]);
211
+ expect(owner.getModifiedEdges()[0].results).toEqual([merged]);
212
+ });
213
+ it("scopes edge modification lookups by modifiedBy", () => {
214
+ const owner = new FakeSceneObject();
215
+ const a = new FakeSceneObject();
216
+ const b = new FakeSceneObject();
217
+ owner.recordModifiedEdges([makeEdge()], [makeEdge()], a);
218
+ owner.recordModifiedEdges([makeEdge()], [makeEdge()], b);
219
+ expect(owner.getModifiedEdges()).toHaveLength(2);
220
+ expect(owner.getModifiedEdges(new Set([a]))).toHaveLength(1);
221
+ expect(owner.getModifiedEdges(new Set([a]))[0].modifiedBy).toBe(a);
222
+ });
223
+ });
224
+ describe("edge removals", () => {
225
+ it("records a removed edge with removedBy", () => {
226
+ const owner = new FakeSceneObject();
227
+ const by = new FakeSceneObject();
228
+ const edge = makeEdge();
229
+ owner.recordRemovedEdge(edge, by);
230
+ expect(owner.getRemovedEdges()).toEqual([edge]);
231
+ });
232
+ it("scopes removal lookups by removedBy", () => {
233
+ const owner = new FakeSceneObject();
234
+ const a = new FakeSceneObject();
235
+ const b = new FakeSceneObject();
236
+ const ea = makeEdge();
237
+ const eb = makeEdge();
238
+ owner.recordRemovedEdge(ea, a);
239
+ owner.recordRemovedEdge(eb, b);
240
+ expect(owner.getRemovedEdges(new Set([a]))).toEqual([ea]);
241
+ expect(owner.getRemovedEdges(new Set([b]))).toEqual([eb]);
242
+ });
243
+ it("propagates to the owning child when called on a container", () => {
244
+ const container = new FakeSceneObject({ container: true });
245
+ const child1 = new FakeSceneObject();
246
+ const child2 = new FakeSceneObject();
247
+ container.addChildObject(child1);
248
+ container.addChildObject(child2);
249
+ const by = new FakeSceneObject();
250
+ const edge = makeEdge();
251
+ child2.recordAddedEdge(edge, child2);
252
+ container.recordRemovedEdge(edge, by);
253
+ expect(child2.getRemovedEdges()).toEqual([edge]);
254
+ expect(child1.getRemovedEdges()).toEqual([]);
255
+ expect(container.getRemovedEdges()).toEqual([]);
256
+ });
257
+ });
258
+ describe("finalShapes", () => {
259
+ it("roundtrips through set/get", () => {
260
+ const owner = new FakeSceneObject();
261
+ const f1 = makeFace();
262
+ const f2 = makeFace();
263
+ owner.setFinalShapes([f1, f2]);
264
+ expect(owner.getFinalShapes()).toEqual([f1, f2]);
265
+ });
266
+ it("replaces prior values on subsequent calls", () => {
267
+ const owner = new FakeSceneObject();
268
+ owner.setFinalShapes([makeFace()]);
269
+ const replacement = makeFace();
270
+ owner.setFinalShapes([replacement]);
271
+ expect(owner.getFinalShapes()).toEqual([replacement]);
272
+ });
273
+ });
274
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getOC } from "../../oc/init.js";
3
+ import { ShapeHistoryTracker } from "../../common/shape-history-tracker.js";
4
+ import { Primitives } from "../../oc/primitives.js";
5
+ import { Explorer } from "../../oc/explorer.js";
6
+ import { Convert } from "../../oc/convert.js";
7
+ function countFaces(shape) {
8
+ const oc = getOC();
9
+ return Explorer.findShapes(shape.getShape(), oc.TopAbs_ShapeEnum.TopAbs_FACE).length;
10
+ }
11
+ function countEdges(shape) {
12
+ const oc = getOC();
13
+ return Explorer.findShapes(shape.getShape(), oc.TopAbs_ShapeEnum.TopAbs_EDGE).length;
14
+ }
15
+ describe("ShapeHistoryTracker", () => {
16
+ describe("BRepBuilderAPI_Transform (translation)", () => {
17
+ it("records every input face as a 1:1 modification on a sphere", () => {
18
+ const oc = getOC();
19
+ const sphere = Primitives.makeSphere(10, 2 * Math.PI);
20
+ const [trsf, disposeTrsf] = Convert.toGpTrsfTranslation(5, 0, 0);
21
+ const transformer = new oc.BRepBuilderAPI_Transform(trsf);
22
+ transformer.Perform(sphere.getShape(), true);
23
+ const history = ShapeHistoryTracker.collect(transformer, [sphere]);
24
+ const inputFaceCount = countFaces(sphere);
25
+ expect(inputFaceCount).toBeGreaterThan(0);
26
+ expect(history.modifiedFaces).toHaveLength(inputFaceCount);
27
+ // Every record should have exactly one source and one result (1:1).
28
+ for (const record of history.modifiedFaces) {
29
+ expect(record.sources).toHaveLength(1);
30
+ expect(record.results).toHaveLength(1);
31
+ }
32
+ expect(history.addedFaces).toEqual([]);
33
+ expect(history.removedFaces).toEqual([]);
34
+ expect(history.generatedFaces).toEqual([]);
35
+ transformer.delete();
36
+ disposeTrsf();
37
+ });
38
+ it("records every input edge as a 1:1 modification on a cylinder", () => {
39
+ const oc = getOC();
40
+ const cyl = Primitives.makeCylinder(5, 10);
41
+ const [trsf, disposeTrsf] = Convert.toGpTrsfTranslation(0, 0, 3);
42
+ const transformer = new oc.BRepBuilderAPI_Transform(trsf);
43
+ transformer.Perform(cyl.getShape(), true);
44
+ const history = ShapeHistoryTracker.collect(transformer, [cyl]);
45
+ const inputFaceCount = countFaces(cyl);
46
+ const inputEdgeCount = countEdges(cyl);
47
+ expect(inputFaceCount).toBeGreaterThan(0);
48
+ expect(inputEdgeCount).toBeGreaterThan(0);
49
+ expect(history.modifiedFaces).toHaveLength(inputFaceCount);
50
+ expect(history.modifiedEdges).toHaveLength(inputEdgeCount);
51
+ for (const record of history.modifiedFaces) {
52
+ expect(record.sources).toHaveLength(1);
53
+ expect(record.results).toHaveLength(1);
54
+ }
55
+ for (const record of history.modifiedEdges) {
56
+ expect(record.sources).toHaveLength(1);
57
+ expect(record.results).toHaveLength(1);
58
+ }
59
+ expect(history.addedFaces).toEqual([]);
60
+ expect(history.addedEdges).toEqual([]);
61
+ expect(history.removedFaces).toEqual([]);
62
+ expect(history.removedEdges).toEqual([]);
63
+ transformer.delete();
64
+ disposeTrsf();
65
+ });
66
+ it("leaves results pointing at the transformed output's subshapes", () => {
67
+ const oc = getOC();
68
+ const sphere = Primitives.makeSphere(8, 2 * Math.PI);
69
+ const [trsf, disposeTrsf] = Convert.toGpTrsfTranslation(1, 1, 1);
70
+ const transformer = new oc.BRepBuilderAPI_Transform(trsf);
71
+ transformer.Perform(sphere.getShape(), true);
72
+ const history = ShapeHistoryTracker.collect(transformer, [sphere]);
73
+ const outputFaces = Explorer.findShapes(transformer.Shape(), oc.TopAbs_ShapeEnum.TopAbs_FACE);
74
+ // Every modification result must correspond to a face in the actual output shape.
75
+ const outputMap = new oc.TopTools_MapOfShape();
76
+ for (const f of outputFaces) {
77
+ outputMap.Add(f);
78
+ }
79
+ for (const record of history.modifiedFaces) {
80
+ for (const result of record.results) {
81
+ expect(outputMap.Contains(result.getShape())).toBe(true);
82
+ }
83
+ }
84
+ outputMap.delete();
85
+ transformer.delete();
86
+ disposeTrsf();
87
+ });
88
+ });
89
+ describe("with no inputs", () => {
90
+ it("returns empty history when no inputs are provided", () => {
91
+ const oc = getOC();
92
+ const sphere = Primitives.makeSphere(3, 2 * Math.PI);
93
+ const [trsf, disposeTrsf] = Convert.toGpTrsfTranslation(1, 0, 0);
94
+ const transformer = new oc.BRepBuilderAPI_Transform(trsf);
95
+ transformer.Perform(sphere.getShape(), true);
96
+ const history = ShapeHistoryTracker.collect(transformer, []);
97
+ // With no inputs, nothing is claimed as modified/generated, so every
98
+ // output face/edge gets classified as added. This is the expected
99
+ // contract: "inputs" defines what the caller considers pre-existing.
100
+ expect(history.modifiedFaces).toEqual([]);
101
+ expect(history.modifiedEdges).toEqual([]);
102
+ expect(history.removedFaces).toEqual([]);
103
+ expect(history.removedEdges).toEqual([]);
104
+ expect(history.addedFaces.length).toBeGreaterThan(0);
105
+ expect(history.addedEdges.length).toBeGreaterThan(0);
106
+ transformer.delete();
107
+ disposeTrsf();
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,69 @@
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 cylinder from "../../../core/cylinder.js";
6
+ import { project, rect, circle } from "../../../core/2d/index.js";
7
+ import { Edge } from "../../../common/edge.js";
8
+ import { EdgeOps } from "../../../oc/edge-ops.js";
9
+ describe("project — regression: all projected edges land on sketch plane", () => {
10
+ setupOC();
11
+ it("projects endFaces of a plain extrude onto z=0 (the sketch plane)", () => {
12
+ sketch("xy", () => {
13
+ rect(100, 50);
14
+ });
15
+ const e = extrude(30);
16
+ const s = sketch("xy", () => {
17
+ project(e.endFaces());
18
+ });
19
+ render();
20
+ const shapes = s.getShapes();
21
+ expect(shapes.length).toBeGreaterThan(0);
22
+ // Every projected edge midpoint should lie on z=0.
23
+ for (const shape of shapes) {
24
+ if (shape instanceof Edge) {
25
+ const mid = EdgeOps.getEdgeMidPoint(shape);
26
+ expect(mid.z).toBeCloseTo(0, 4);
27
+ }
28
+ }
29
+ });
30
+ it("projects endFaces of an extrude fused with a cylinder onto z=0", () => {
31
+ // Mirrors the user's scenario: complex shape built by fusing an extrude
32
+ // with an existing cylinder, then projecting faces back onto the sketch plane.
33
+ cylinder(15, 40);
34
+ sketch("xy", () => {
35
+ rect(80, 40);
36
+ });
37
+ const e = extrude(20);
38
+ const s = sketch("xy", () => {
39
+ project(e.endFaces());
40
+ });
41
+ render();
42
+ const shapes = s.getShapes();
43
+ expect(shapes.length).toBeGreaterThan(0);
44
+ for (const shape of shapes) {
45
+ if (shape instanceof Edge) {
46
+ const mid = EdgeOps.getEdgeMidPoint(shape);
47
+ expect(mid.z).toBeCloseTo(0, 4);
48
+ }
49
+ }
50
+ });
51
+ it("projects sideFaces including a cylindrical one onto z=0", () => {
52
+ sketch("xy", () => {
53
+ circle([0, 0], 20);
54
+ });
55
+ const e = extrude(30);
56
+ const s = sketch("xy", () => {
57
+ project(e.sideFaces());
58
+ });
59
+ render();
60
+ const shapes = s.getShapes();
61
+ expect(shapes.length).toBeGreaterThan(0);
62
+ for (const shape of shapes) {
63
+ if (shape instanceof Edge) {
64
+ const mid = EdgeOps.getEdgeMidPoint(shape);
65
+ expect(mid.z).toBeCloseTo(0, 4);
66
+ }
67
+ }
68
+ });
69
+ });
@@ -0,0 +1,37 @@
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 plane from "../../../core/plane.js";
6
+ import { project, arc, connect, move } from "../../../core/2d/index.js";
7
+ import { edge } from "../../../filters/index.js";
8
+ import { Edge } from "../../../common/edge.js";
9
+ import { EdgeOps } from "../../../oc/edge-ops.js";
10
+ describe("project — user regression (symmetric arc extrude onto offset front plane)", () => {
11
+ setupOC();
12
+ it("projects arc end-edges of a symmetric extrude onto an offset front plane", () => {
13
+ sketch("front", () => {
14
+ arc(31);
15
+ connect();
16
+ move([0, 0]);
17
+ });
18
+ const circleExtrude = extrude(66).symmetric();
19
+ const p = plane("front", { offset: 20 });
20
+ const s = sketch(p, () => {
21
+ project(circleExtrude.endEdges(edge().arc()));
22
+ });
23
+ render();
24
+ const shapes = s.getShapes();
25
+ expect(shapes.length).toBeGreaterThan(0);
26
+ // Every projected edge must lie on the target plane `p`. Use the plane's
27
+ // containsPoint check to avoid hardcoding coordinate-system assumptions.
28
+ const targetPlane = p.getPlane();
29
+ for (const shape of shapes) {
30
+ if (shape instanceof Edge) {
31
+ const mid = EdgeOps.getEdgeMidPoint(shape);
32
+ const dist = targetPlane.distanceToPoint(mid);
33
+ expect(dist).toBeLessThan(1e-4);
34
+ }
35
+ }
36
+ });
37
+ });
@@ -0,0 +1 @@
1
+ export {};