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.
- package/lib/dist/common/scene-object.d.ts +45 -0
- package/lib/dist/common/scene-object.js +121 -0
- package/lib/dist/common/shape-factory.d.ts +1 -1
- package/lib/dist/common/shape-history-tracker.d.ts +35 -0
- package/lib/dist/common/shape-history-tracker.js +114 -0
- package/lib/dist/common/shape.js +7 -1
- package/lib/dist/common/shapes.d.ts +0 -1
- package/lib/dist/common/shapes.js +0 -1
- package/lib/dist/common/solid.js +5 -1
- package/lib/dist/core/extrude.d.ts +12 -13
- package/lib/dist/core/extrude.js +19 -1
- package/lib/dist/core/part.d.ts +2 -1
- package/lib/dist/core/part.js +4 -1
- package/lib/dist/core/sketch.d.ts +4 -3
- package/lib/dist/core/sketch.js +4 -1
- package/lib/dist/features/chamfer.js +12 -6
- package/lib/dist/features/extrude-base.d.ts +43 -1
- package/lib/dist/features/extrude-base.js +141 -36
- package/lib/dist/features/extrude-to-face.d.ts +1 -1
- package/lib/dist/features/extrude-to-face.js +42 -19
- package/lib/dist/features/extrude-two-distances.d.ts +1 -1
- package/lib/dist/features/extrude-two-distances.js +41 -15
- package/lib/dist/features/extrude.d.ts +1 -1
- package/lib/dist/features/extrude.js +75 -20
- package/lib/dist/features/fillet.js +3 -4
- package/lib/dist/features/fuse.js +14 -0
- package/lib/dist/features/infinite-extrude.d.ts +1 -0
- package/lib/dist/features/infinite-extrude.js +33 -4
- package/lib/dist/features/loft.js +18 -5
- package/lib/dist/features/mirror-shape.d.ts +1 -3
- package/lib/dist/features/mirror-shape.js +2 -1
- package/lib/dist/features/revolve.js +17 -4
- package/lib/dist/features/rotate.js +1 -0
- package/lib/dist/features/simple-extruder.js +5 -0
- package/lib/dist/features/sweep.js +13 -2
- package/lib/dist/features/translate.js +3 -1
- package/lib/dist/filters/face/face-filter.d.ts +12 -0
- package/lib/dist/filters/face/face-filter.js +21 -0
- package/lib/dist/filters/face/torus-filter.d.ts +19 -0
- package/lib/dist/filters/face/torus-filter.js +38 -0
- package/lib/dist/helpers/scene-helpers.d.ts +10 -2
- package/lib/dist/helpers/scene-helpers.js +278 -10
- package/lib/dist/index.d.ts +1 -0
- package/lib/dist/oc/boolean-ops.d.ts +32 -4
- package/lib/dist/oc/boolean-ops.js +122 -11
- package/lib/dist/oc/color-transfer.d.ts +37 -0
- package/lib/dist/oc/color-transfer.js +135 -0
- package/lib/dist/oc/extrude-ops.js +25 -3
- package/lib/dist/oc/face-ops.d.ts +0 -1
- package/lib/dist/oc/face-ops.js +0 -13
- package/lib/dist/oc/face-query.d.ts +2 -0
- package/lib/dist/oc/face-query.js +30 -0
- package/lib/dist/oc/fillet-ops.d.ts +5 -3
- package/lib/dist/oc/fillet-ops.js +107 -70
- package/lib/dist/oc/intersection.js +6 -3
- package/lib/dist/oc/mesh.d.ts +25 -2
- package/lib/dist/oc/mesh.js +112 -35
- package/lib/dist/oc/shape-ops.d.ts +25 -20
- package/lib/dist/oc/shape-ops.js +129 -113
- package/lib/dist/rendering/mesh-transform.js +17 -1
- package/lib/dist/rendering/render-solid.js +19 -6
- package/lib/dist/rendering/render-wire.js +2 -0
- package/lib/dist/rendering/render.d.ts +12 -2
- package/lib/dist/rendering/render.js +195 -220
- package/lib/dist/scene-manager.d.ts +2 -0
- package/lib/dist/scene-manager.js +4 -3
- package/lib/dist/tests/common/scene-object-history.test.d.ts +1 -0
- package/lib/dist/tests/common/scene-object-history.test.js +274 -0
- package/lib/dist/tests/common/shape-history-tracker.test.d.ts +1 -0
- package/lib/dist/tests/common/shape-history-tracker.test.js +110 -0
- package/lib/dist/tests/features/2d/project-regression.test.d.ts +1 -0
- package/lib/dist/tests/features/2d/project-regression.test.js +69 -0
- package/lib/dist/tests/features/2d/project-user-regression.test.d.ts +1 -0
- package/lib/dist/tests/features/2d/project-user-regression.test.js +37 -0
- package/lib/dist/tests/features/color-lineage.test.d.ts +1 -0
- package/lib/dist/tests/features/color-lineage.test.js +213 -0
- package/lib/dist/tests/features/cut-symmetric-through-all.test.d.ts +1 -0
- package/lib/dist/tests/features/cut-symmetric-through-all.test.js +32 -0
- package/lib/dist/tests/features/extrude-history.test.d.ts +1 -0
- package/lib/dist/tests/features/extrude-history.test.js +248 -0
- package/lib/dist/tests/features/extrude.test.js +71 -0
- package/lib/dist/tests/features/fillet2d.test.js +16 -1
- package/lib/dist/tests/features/peer-ops-history.test.d.ts +1 -0
- package/lib/dist/tests/features/peer-ops-history.test.js +119 -0
- package/lib/dist/tests/features/select.test.js +50 -0
- package/lib/dist/tests/features/subtract.test.js +21 -1
- package/lib/dist/tests/setup.js +3 -2
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/ui/dist/assets/{index-BeLxRMCv.js → index-BrW_x4uc.js} +37 -37
- package/ui/dist/index.html +1 -1
- package/lib/dist/common/solid-face.d.ts +0 -9
- package/lib/dist/common/solid-face.js +0 -22
|
@@ -9,20 +9,32 @@ import { ExtrudeThroughAll } from "./infinite-extrude.js";
|
|
|
9
9
|
import { ThinFaceMaker } from "../oc/thin-face-maker.js";
|
|
10
10
|
export class Extrude extends ExtrudeBase {
|
|
11
11
|
distance;
|
|
12
|
-
constructor(distance,
|
|
13
|
-
super(
|
|
12
|
+
constructor(distance, source) {
|
|
13
|
+
super(source);
|
|
14
14
|
this.distance = distance;
|
|
15
15
|
}
|
|
16
16
|
build(context) {
|
|
17
|
-
const
|
|
17
|
+
const tBuild = performance.now();
|
|
18
|
+
let t = performance.now();
|
|
19
|
+
const plane = this.getSourcePlane();
|
|
20
|
+
console.log(`[perf] Extrude.getSourcePlane: ${(performance.now() - t).toFixed(1)} ms`);
|
|
21
|
+
t = performance.now();
|
|
18
22
|
const pickedFaces = this.resolvePickedFaces(plane);
|
|
23
|
+
console.log(`[perf] Extrude.resolvePickedFaces: ${(performance.now() - t).toFixed(1)} ms`);
|
|
19
24
|
if (pickedFaces !== null && pickedFaces.length === 0) {
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
27
|
let faces;
|
|
23
28
|
let inwardEdges;
|
|
24
29
|
let outwardEdges;
|
|
25
|
-
|
|
30
|
+
t = performance.now();
|
|
31
|
+
if (this.isFaceSourced()) {
|
|
32
|
+
if (this.isThin()) {
|
|
33
|
+
throw new Error("thin() is not supported with a face-sourced extrude");
|
|
34
|
+
}
|
|
35
|
+
faces = pickedFaces ?? this.getSourceFaces();
|
|
36
|
+
}
|
|
37
|
+
else if (this.isThin()) {
|
|
26
38
|
const thinResult = ThinFaceMaker.make(this.extrudable.getGeometries(), plane, this._thin[0], this._thin[1]);
|
|
27
39
|
faces = thinResult.faces;
|
|
28
40
|
inwardEdges = thinResult.inwardEdges;
|
|
@@ -31,6 +43,7 @@ export class Extrude extends ExtrudeBase {
|
|
|
31
43
|
else {
|
|
32
44
|
faces = pickedFaces ?? FaceMaker2.getRegions(this.extrudable.getGeometries(), plane, this.getDrill());
|
|
33
45
|
}
|
|
46
|
+
console.log(`[perf] Extrude.resolveFaces (faces=${faces.length}, faceSourced=${this.isFaceSourced()}): ${(performance.now() - t).toFixed(1)} ms`);
|
|
34
47
|
if (this._operationMode === 'remove') {
|
|
35
48
|
this.buildRemove(faces, plane, context);
|
|
36
49
|
}
|
|
@@ -40,11 +53,17 @@ export class Extrude extends ExtrudeBase {
|
|
|
40
53
|
else {
|
|
41
54
|
this.buildAdd(faces, plane, context, inwardEdges, outwardEdges);
|
|
42
55
|
}
|
|
56
|
+
this.setFinalShapes(this.getShapes());
|
|
57
|
+
console.log(`[perf] Extrude.build TOTAL: ${(performance.now() - tBuild).toFixed(1)} ms`);
|
|
43
58
|
}
|
|
44
59
|
buildAdd(faces, plane, context, inwardEdges, outwardEdges) {
|
|
60
|
+
let t = performance.now();
|
|
45
61
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
62
|
+
console.log(`[perf] Extrude.buildAdd.resolveFusionScope (n=${sceneObjects.length}): ${(performance.now() - t).toFixed(1)} ms`);
|
|
63
|
+
t = performance.now();
|
|
46
64
|
const extruder = new Extruder(faces, plane, this.distance, this.getDraft(), this.getEndOffset());
|
|
47
65
|
let extrusions = extruder.extrude();
|
|
66
|
+
console.log(`[perf] Extrude.buildAdd.extruder.extrude (extrusions=${extrusions.length}): ${(performance.now() - t).toFixed(1)} ms`);
|
|
48
67
|
let sideFaces = extruder.getSideFaces();
|
|
49
68
|
let internalFaces = extruder.getInternalFaces();
|
|
50
69
|
let capFaces = [];
|
|
@@ -59,12 +78,20 @@ export class Extrude extends ExtrudeBase {
|
|
|
59
78
|
this.setState('side-faces', sideFaces);
|
|
60
79
|
this.setState('internal-faces', internalFaces);
|
|
61
80
|
this.setState('cap-faces', capFaces);
|
|
62
|
-
this.
|
|
81
|
+
this.getSource()?.removeShapes(this);
|
|
82
|
+
console.log("Extrusions before fusion:", extrusions.length);
|
|
63
83
|
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
64
84
|
this.addShapes(extrusions);
|
|
85
|
+
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
86
|
+
this.classifyExtrudeEdges();
|
|
65
87
|
return;
|
|
66
88
|
}
|
|
67
|
-
const
|
|
89
|
+
const tFuse = performance.now();
|
|
90
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
91
|
+
glue: this.isFaceSourced() ? 'full' : undefined,
|
|
92
|
+
recordHistoryFor: this,
|
|
93
|
+
});
|
|
94
|
+
console.log(`[perf] Extrude.buildAdd.fuseWithSceneObjects: ${(performance.now() - tFuse).toFixed(1)} ms`);
|
|
68
95
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
69
96
|
if (!modifiedShape.object) {
|
|
70
97
|
continue;
|
|
@@ -72,6 +99,10 @@ export class Extrude extends ExtrudeBase {
|
|
|
72
99
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
73
100
|
}
|
|
74
101
|
this.addShapes(fusionResult.newShapes);
|
|
102
|
+
if (fusionResult.toolHistory) {
|
|
103
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
104
|
+
}
|
|
105
|
+
this.classifyExtrudeEdges();
|
|
75
106
|
}
|
|
76
107
|
buildSymmetric(faces, plane, context, inwardEdges, outwardEdges) {
|
|
77
108
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
@@ -82,7 +113,9 @@ export class Extrude extends ExtrudeBase {
|
|
|
82
113
|
const extrusions2 = extruder2.extrude();
|
|
83
114
|
const endFaces = extruder2.getEndFaces();
|
|
84
115
|
const all = [...extrusions1, ...extrusions2];
|
|
85
|
-
const
|
|
116
|
+
const halvesFuse = BooleanOps.fuse(all);
|
|
117
|
+
const extrusions = halvesFuse.result;
|
|
118
|
+
halvesFuse.dispose();
|
|
86
119
|
// Collect remaining faces and fused start/end faces from the fused solid.
|
|
87
120
|
// We need the fused face objects (not pre-fusion) for IsPartner matching.
|
|
88
121
|
const remainingFaces = [];
|
|
@@ -158,12 +191,16 @@ export class Extrude extends ExtrudeBase {
|
|
|
158
191
|
this.setState('side-faces', sideFaces);
|
|
159
192
|
this.setState('internal-faces', internalFaces);
|
|
160
193
|
this.setState('cap-faces', capFaces);
|
|
161
|
-
this.
|
|
194
|
+
this.getSource()?.removeShapes(this);
|
|
162
195
|
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
163
196
|
this.addShapes(extrusions);
|
|
197
|
+
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
198
|
+
this.classifyExtrudeEdges();
|
|
164
199
|
return;
|
|
165
200
|
}
|
|
166
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions
|
|
201
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
202
|
+
recordHistoryFor: this,
|
|
203
|
+
});
|
|
167
204
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
168
205
|
if (!modifiedShape.object) {
|
|
169
206
|
continue;
|
|
@@ -171,6 +208,10 @@ export class Extrude extends ExtrudeBase {
|
|
|
171
208
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
172
209
|
}
|
|
173
210
|
this.addShapes(fusionResult.newShapes);
|
|
211
|
+
if (fusionResult.toolHistory) {
|
|
212
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
213
|
+
}
|
|
214
|
+
this.classifyExtrudeEdges();
|
|
174
215
|
}
|
|
175
216
|
buildRemove(faces, plane, context) {
|
|
176
217
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
@@ -179,6 +220,9 @@ export class Extrude extends ExtrudeBase {
|
|
|
179
220
|
if (this._symmetric) {
|
|
180
221
|
// Symmetric cut: create tool centered on sketch plane
|
|
181
222
|
if (isThroughAll) {
|
|
223
|
+
if (this.isFaceSourced()) {
|
|
224
|
+
throw new Error("through-all is not supported with a face-sourced extrude");
|
|
225
|
+
}
|
|
182
226
|
const extrudeThroughAll = new ExtrudeThroughAll(this.extrudable, true, true, faces);
|
|
183
227
|
toolShapes = extrudeThroughAll.build();
|
|
184
228
|
}
|
|
@@ -188,11 +232,15 @@ export class Extrude extends ExtrudeBase {
|
|
|
188
232
|
const extruder2 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset());
|
|
189
233
|
const extrusions2 = extruder2.extrude();
|
|
190
234
|
const all = [...extrusions1, ...extrusions2];
|
|
191
|
-
const
|
|
192
|
-
toolShapes = result;
|
|
235
|
+
const halvesFuse = BooleanOps.fuse(all);
|
|
236
|
+
toolShapes = halvesFuse.result;
|
|
237
|
+
halvesFuse.dispose();
|
|
193
238
|
}
|
|
194
239
|
}
|
|
195
240
|
else if (isThroughAll) {
|
|
241
|
+
if (this.isFaceSourced()) {
|
|
242
|
+
throw new Error("through-all is not supported with a face-sourced extrude");
|
|
243
|
+
}
|
|
196
244
|
const extrudeThroughAll = new ExtrudeThroughAll(this.extrudable, false, true, faces);
|
|
197
245
|
toolShapes = extrudeThroughAll.build();
|
|
198
246
|
}
|
|
@@ -201,17 +249,19 @@ export class Extrude extends ExtrudeBase {
|
|
|
201
249
|
const extruder = new Extruder(faces, plane, distance, this.getDraft(), this.getEndOffset());
|
|
202
250
|
toolShapes = extruder.extrude();
|
|
203
251
|
}
|
|
204
|
-
this.
|
|
205
|
-
cutWithSceneObjects(scope, toolShapes, plane, this.distance, this
|
|
252
|
+
this.getSource()?.removeShapes(this);
|
|
253
|
+
cutWithSceneObjects(scope, toolShapes, plane, this.distance, this, {
|
|
254
|
+
recordHistoryFor: this,
|
|
255
|
+
});
|
|
206
256
|
}
|
|
207
257
|
getDependencies() {
|
|
208
|
-
|
|
258
|
+
const source = this.getSource();
|
|
259
|
+
return source ? [source] : [];
|
|
209
260
|
}
|
|
210
261
|
createCopy(remap) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return new Extrude(this.distance, extrudable).syncWith(this);
|
|
262
|
+
const source = this.getSource();
|
|
263
|
+
const remapped = source ? (remap.get(source) || source) : undefined;
|
|
264
|
+
return new Extrude(this.distance, remapped).syncWith(this);
|
|
215
265
|
}
|
|
216
266
|
compareTo(other) {
|
|
217
267
|
if (!(other instanceof Extrude)) {
|
|
@@ -220,7 +270,12 @@ export class Extrude extends ExtrudeBase {
|
|
|
220
270
|
if (!super.compareTo(other)) {
|
|
221
271
|
return false;
|
|
222
272
|
}
|
|
223
|
-
|
|
273
|
+
const thisSource = this.getSource();
|
|
274
|
+
const otherSource = other.getSource();
|
|
275
|
+
if (!thisSource !== !otherSource) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (thisSource && otherSource && !thisSource.compareTo(otherSource)) {
|
|
224
279
|
return false;
|
|
225
280
|
}
|
|
226
281
|
if (this.distance !== other.distance) {
|
|
@@ -242,7 +297,7 @@ export class Extrude extends ExtrudeBase {
|
|
|
242
297
|
}
|
|
243
298
|
serialize() {
|
|
244
299
|
return {
|
|
245
|
-
extrudable: this.
|
|
300
|
+
extrudable: this.getSource()?.serialize(),
|
|
246
301
|
distance: this.distance,
|
|
247
302
|
operationMode: this._operationMode !== 'add' ? this._operationMode : undefined,
|
|
248
303
|
symmetric: this._symmetric || undefined,
|
|
@@ -65,12 +65,11 @@ export class Fillet extends SceneObject {
|
|
|
65
65
|
}
|
|
66
66
|
edges = edges.filter(e => !targetEdges.includes(e));
|
|
67
67
|
try {
|
|
68
|
-
const
|
|
68
|
+
const newSolids = FilletOps.makeFillet(solid, targetEdges, this.radius);
|
|
69
69
|
const obj = sceneShapeObjectMap.get(shape);
|
|
70
70
|
removedShapes.push({ shape: solid, owner: obj });
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
addedShapes.push(subShape);
|
|
71
|
+
for (const newSolid of newSolids) {
|
|
72
|
+
addedShapes.push(newSolid);
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
catch {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SceneObject } from "../common/scene-object.js";
|
|
2
2
|
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
3
|
+
import { ColorTransfer } from "../oc/color-transfer.js";
|
|
3
4
|
export class Fuse extends SceneObject {
|
|
4
5
|
_sceneObjects = [];
|
|
5
6
|
constructor(...objects) {
|
|
@@ -26,16 +27,29 @@ export class Fuse extends SceneObject {
|
|
|
26
27
|
}
|
|
27
28
|
const fuseResult = BooleanOps.fuse(allShapes);
|
|
28
29
|
if (fuseResult.result.length === allShapes.length) {
|
|
30
|
+
fuseResult.dispose();
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
33
|
if (!fuseResult.modifiedShapes.length) {
|
|
34
|
+
fuseResult.dispose();
|
|
32
35
|
return;
|
|
33
36
|
}
|
|
37
|
+
// Color rule for the user-facing Fuse op: the FIRST input is dominant.
|
|
38
|
+
// If it has any colors, propagate those to the result (and bleed across
|
|
39
|
+
// adjacent faces from any uncolored input). If the first input has no
|
|
40
|
+
// colors, the fused result stays uncolored — even if other inputs are
|
|
41
|
+
// colored, those colors are intentionally dropped.
|
|
42
|
+
const firstShape = allShapes[0];
|
|
43
|
+
if (firstShape && firstShape.hasColors()) {
|
|
44
|
+
ColorTransfer.applyThroughMaker([firstShape], fuseResult.newShapes, fuseResult.maker);
|
|
45
|
+
ColorTransfer.applyBleeding([firstShape], fuseResult.newShapes, fuseResult.maker);
|
|
46
|
+
}
|
|
34
47
|
for (const shape of fuseResult.modifiedShapes) {
|
|
35
48
|
const obj = objShapeMap.get(shape);
|
|
36
49
|
obj.removeShape(shape, this);
|
|
37
50
|
}
|
|
38
51
|
this.addShapes(fuseResult.newShapes);
|
|
52
|
+
fuseResult.dispose();
|
|
39
53
|
}
|
|
40
54
|
compareTo(other) {
|
|
41
55
|
if (!(other instanceof Fuse)) {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { Solid } from "../common/shapes.js";
|
|
1
2
|
import { ExtrudeOps } from "../oc/extrude-ops.js";
|
|
2
3
|
import { FaceMaker2 } from "../oc/face-maker2.js";
|
|
4
|
+
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
5
|
+
import { Explorer } from "../oc/explorer.js";
|
|
6
|
+
/** A large finite magnitude that stands in for "infinity" in through-all ops.
|
|
7
|
+
* True infinite prisms (via OC's `Inf=true` flag) silently fail inside
|
|
8
|
+
* `BRepAlgoAPI_Cut` — use a large finite extrusion instead. */
|
|
9
|
+
const THROUGH_ALL_LENGTH = 100000;
|
|
3
10
|
export class ExtrudeThroughAll {
|
|
4
11
|
extrudable;
|
|
5
12
|
symmetric;
|
|
@@ -25,19 +32,29 @@ export class ExtrudeThroughAll {
|
|
|
25
32
|
dir = dir.multiply(-1);
|
|
26
33
|
}
|
|
27
34
|
const shouldDispose = !this.pickedFaces;
|
|
35
|
+
const bigDir = dir.multiply(THROUGH_ALL_LENGTH);
|
|
28
36
|
if (this.symmetric) {
|
|
29
37
|
for (const face of faces) {
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
// Fuse two finite large prisms — one in each direction — to approximate
|
|
39
|
+
// a symmetric through-all tool. Cannot use makePrismSymmetric here
|
|
40
|
+
// because `BRepAlgoAPI_Cut` silently drops infinite shapes.
|
|
41
|
+
const positive = ExtrudeOps.makePrism(face, bigDir, 1);
|
|
42
|
+
const negative = ExtrudeOps.makePrism(face, bigDir, -1);
|
|
43
|
+
const fuseResult = BooleanOps.fuse([positive, negative]);
|
|
44
|
+
const fusedSolid = fuseResult.result.find(s => s.getType() === 'solid')
|
|
45
|
+
?? this.firstSolidOf(fuseResult.result);
|
|
46
|
+
if (fusedSolid) {
|
|
47
|
+
solids.push(fusedSolid);
|
|
48
|
+
}
|
|
49
|
+
fuseResult.dispose();
|
|
32
50
|
if (shouldDispose) {
|
|
33
51
|
face.dispose();
|
|
34
52
|
}
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
else {
|
|
38
|
-
dir = dir.multiply(100000);
|
|
39
56
|
for (const face of faces) {
|
|
40
|
-
const solid = ExtrudeOps.makePrism(face,
|
|
57
|
+
const solid = ExtrudeOps.makePrism(face, bigDir, 1);
|
|
41
58
|
solids.push(solid);
|
|
42
59
|
if (shouldDispose) {
|
|
43
60
|
face.dispose();
|
|
@@ -47,4 +64,16 @@ export class ExtrudeThroughAll {
|
|
|
47
64
|
this.shapes = solids;
|
|
48
65
|
return solids;
|
|
49
66
|
}
|
|
67
|
+
firstSolidOf(shapes) {
|
|
68
|
+
for (const shape of shapes) {
|
|
69
|
+
if (shape.getType() === 'solid') {
|
|
70
|
+
return shape;
|
|
71
|
+
}
|
|
72
|
+
const subSolids = Explorer.findShapes(shape.getShape(), Explorer.getOcShapeType('solid'));
|
|
73
|
+
if (subSolids.length > 0) {
|
|
74
|
+
return Solid.fromTopoDSSolid(Explorer.toSolid(subSolids[0]));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
50
79
|
}
|
|
@@ -66,21 +66,32 @@ export class Loft extends ExtrudeBase {
|
|
|
66
66
|
if (this._operationMode === 'remove') {
|
|
67
67
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
68
68
|
const plane = firstPlane || lastPlane;
|
|
69
|
-
cutWithSceneObjects(scope, newShapes, plane, 0, this);
|
|
69
|
+
cutWithSceneObjects(scope, newShapes, plane, 0, this, { recordHistoryFor: this });
|
|
70
|
+
this.setFinalShapes(this.getShapes());
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
73
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
73
74
|
if (sceneObjects.length === 0) {
|
|
74
75
|
this.addShapes(newShapes);
|
|
76
|
+
this.recordShapeFacesAndEdgesAsAdditions(newShapes);
|
|
77
|
+
this.classifyExtrudeEdges();
|
|
78
|
+
this.setFinalShapes(this.getShapes());
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes
|
|
81
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes, {
|
|
82
|
+
recordHistoryFor: this,
|
|
83
|
+
});
|
|
78
84
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
79
85
|
if (modifiedShape.object) {
|
|
80
86
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
this.addShapes(fusionResult.newShapes);
|
|
90
|
+
if (fusionResult.toolHistory) {
|
|
91
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
92
|
+
}
|
|
93
|
+
this.classifyExtrudeEdges();
|
|
94
|
+
this.setFinalShapes(this.getShapes());
|
|
84
95
|
}
|
|
85
96
|
buildThinLoft() {
|
|
86
97
|
const outerWires = [];
|
|
@@ -103,9 +114,11 @@ export class Loft extends ExtrudeBase {
|
|
|
103
114
|
const outerSolids = LoftOps.makeLoft(outerWires);
|
|
104
115
|
if (innerWires.length > 0 && innerWires.length === outerWires.length) {
|
|
105
116
|
const innerSolids = LoftOps.makeLoft(innerWires);
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const cutResult = BooleanOps.cutShapes(
|
|
117
|
+
const outerFuse = BooleanOps.fuse(outerSolids);
|
|
118
|
+
const innerFuse = BooleanOps.fuse(innerSolids);
|
|
119
|
+
const cutResult = BooleanOps.cutShapes(outerFuse.result[0], innerFuse.result[0]);
|
|
120
|
+
outerFuse.dispose();
|
|
121
|
+
innerFuse.dispose();
|
|
109
122
|
return [cutResult];
|
|
110
123
|
}
|
|
111
124
|
return outerSolids;
|
|
@@ -47,6 +47,7 @@ export class MirrorShape extends SceneObject {
|
|
|
47
47
|
for (const shape of shapes) {
|
|
48
48
|
const matrix = Matrix4.mirrorPlane(plane.normal, plane.origin);
|
|
49
49
|
const transformed = ShapeOps.transform(shape, matrix);
|
|
50
|
+
transformed.setMeshSource(shape, matrix);
|
|
50
51
|
transformedShapes.push(transformed);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -90,7 +91,7 @@ export class MirrorShape extends SceneObject {
|
|
|
90
91
|
}
|
|
91
92
|
serialize() {
|
|
92
93
|
return {
|
|
93
|
-
|
|
94
|
+
// plane: this.plane,
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -9,6 +9,7 @@ import { ExtrudeBase } from "./extrude-base.js";
|
|
|
9
9
|
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
10
10
|
import { FaceOps } from "../oc/face-ops.js";
|
|
11
11
|
import { ThinFaceMaker } from "../oc/thin-face-maker.js";
|
|
12
|
+
import { Matrix4 } from "../math/matrix4.js";
|
|
12
13
|
export class Revolve extends ExtrudeBase {
|
|
13
14
|
axis;
|
|
14
15
|
angle;
|
|
@@ -45,8 +46,9 @@ export class Revolve extends ExtrudeBase {
|
|
|
45
46
|
const solid = ExtrudeOps.makeRevol(face, axis, rad(this.angle));
|
|
46
47
|
let resultSolid;
|
|
47
48
|
if (this._symmetric) {
|
|
48
|
-
const
|
|
49
|
-
|
|
49
|
+
const matrix = Matrix4.fromRotationAroundAxis(axis.origin, axis.direction, -rad(this.angle) / 2);
|
|
50
|
+
const rotated = ShapeOps.transform(solid, matrix);
|
|
51
|
+
resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(rotated.getShape()));
|
|
50
52
|
}
|
|
51
53
|
else {
|
|
52
54
|
resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(solid.getShape()));
|
|
@@ -113,21 +115,32 @@ export class Revolve extends ExtrudeBase {
|
|
|
113
115
|
this.axis.removeShapes(this);
|
|
114
116
|
if (this._operationMode === 'remove') {
|
|
115
117
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
116
|
-
cutWithSceneObjects(scope, solids, plane, 0, this);
|
|
118
|
+
cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
|
|
119
|
+
this.setFinalShapes(this.getShapes());
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
119
122
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
120
123
|
if (sceneObjects.length === 0) {
|
|
121
124
|
this.addShapes(solids);
|
|
125
|
+
this.recordShapeFacesAndEdgesAsAdditions(solids);
|
|
126
|
+
this.classifyExtrudeEdges();
|
|
127
|
+
this.setFinalShapes(this.getShapes());
|
|
122
128
|
return;
|
|
123
129
|
}
|
|
124
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, solids
|
|
130
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
|
|
131
|
+
recordHistoryFor: this,
|
|
132
|
+
});
|
|
125
133
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
126
134
|
if (modifiedShape.object) {
|
|
127
135
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
this.addShapes(fusionResult.newShapes);
|
|
139
|
+
if (fusionResult.toolHistory) {
|
|
140
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
141
|
+
}
|
|
142
|
+
this.classifyExtrudeEdges();
|
|
143
|
+
this.setFinalShapes(this.getShapes());
|
|
131
144
|
}
|
|
132
145
|
getDependencies() {
|
|
133
146
|
return this.extrudable ? [this.extrudable] : [];
|
|
@@ -41,6 +41,7 @@ export class Rotate extends SceneObject {
|
|
|
41
41
|
const shapes = obj.getShapes();
|
|
42
42
|
for (const shape of shapes) {
|
|
43
43
|
const transformed = ShapeOps.transform(shape, matrix);
|
|
44
|
+
transformed.setMeshSource(shape, matrix);
|
|
44
45
|
this.addShape(transformed);
|
|
45
46
|
if (!this.copy) {
|
|
46
47
|
obj.removeShape(shape, this);
|
|
@@ -42,9 +42,14 @@ export class Extruder {
|
|
|
42
42
|
let lastFaces = [];
|
|
43
43
|
let sideFaces = [];
|
|
44
44
|
let internalFaces = [];
|
|
45
|
+
console.log("Fusing faces before extrusion...", this.faces.length);
|
|
46
|
+
const tFuseFaces = performance.now();
|
|
45
47
|
const fusedFaces = BooleanOps.fuseFaces(this.faces);
|
|
48
|
+
console.log(`[perf] Extruder.fuseFaces (in=${this.faces.length}, out=${fusedFaces.result.length}): ${(performance.now() - tFuseFaces).toFixed(1)} ms`);
|
|
46
49
|
for (const face of fusedFaces.result) {
|
|
50
|
+
const time = performance.now();
|
|
47
51
|
let { solid, firstFace, lastFace } = ExtrudeOps.makePrismFromVec(face, vec);
|
|
52
|
+
console.log(`[perf] Extruder.makePrismFromVec: ${(performance.now() - time).toFixed(1)} ms`);
|
|
48
53
|
if (this.draft) {
|
|
49
54
|
const draftResult = this.applyDraft(solid, firstFace, lastFace, this.plane);
|
|
50
55
|
solid = draftResult.solid;
|
|
@@ -102,21 +102,32 @@ export class Sweep extends ExtrudeBase {
|
|
|
102
102
|
// Handle boolean operation based on operation mode
|
|
103
103
|
if (this._operationMode === 'remove') {
|
|
104
104
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
105
|
-
cutWithSceneObjects(scope, newShapes, plane, 0, this);
|
|
105
|
+
cutWithSceneObjects(scope, newShapes, plane, 0, this, { recordHistoryFor: this });
|
|
106
|
+
this.setFinalShapes(this.getShapes());
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
109
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
109
110
|
if (sceneObjects.length === 0) {
|
|
110
111
|
this.addShapes(newShapes);
|
|
112
|
+
this.recordShapeFacesAndEdgesAsAdditions(newShapes);
|
|
113
|
+
this.classifyExtrudeEdges();
|
|
114
|
+
this.setFinalShapes(this.getShapes());
|
|
111
115
|
return;
|
|
112
116
|
}
|
|
113
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes
|
|
117
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, newShapes, {
|
|
118
|
+
recordHistoryFor: this,
|
|
119
|
+
});
|
|
114
120
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
115
121
|
if (modifiedShape.object) {
|
|
116
122
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
this.addShapes(fusionResult.newShapes);
|
|
126
|
+
if (fusionResult.toolHistory) {
|
|
127
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
128
|
+
}
|
|
129
|
+
this.classifyExtrudeEdges();
|
|
130
|
+
this.setFinalShapes(this.getShapes());
|
|
120
131
|
}
|
|
121
132
|
getSpineWire(pathObj) {
|
|
122
133
|
const shapes = pathObj.getShapes({ excludeMeta: false });
|
|
@@ -23,7 +23,9 @@ export class Translate extends SceneObject {
|
|
|
23
23
|
continue;
|
|
24
24
|
}
|
|
25
25
|
const amount = this.amount.asPoint();
|
|
26
|
-
|
|
26
|
+
const matrix = Matrix4.fromTranslation(amount.x, amount.y, amount.z);
|
|
27
|
+
const transformed = ShapeOps.transform(shape, matrix);
|
|
28
|
+
transformed.setMeshSource(shape, matrix);
|
|
27
29
|
this.addShape(transformed);
|
|
28
30
|
if (!this.copy) {
|
|
29
31
|
obj.removeShape(shape, this);
|
|
@@ -58,6 +58,18 @@ export declare class FaceFilterBuilder extends FilterBuilderBase<Face> {
|
|
|
58
58
|
* @param plane - The reference plane.
|
|
59
59
|
*/
|
|
60
60
|
notParallelTo(plane: PlaneLike | PlaneObjectBase): this;
|
|
61
|
+
/**
|
|
62
|
+
* Selects toroidal faces, optionally matching major and/or minor radius.
|
|
63
|
+
* @param majorRadius - Optional radius from the torus axis to the tube center.
|
|
64
|
+
* @param minorRadius - Optional radius of the tube itself.
|
|
65
|
+
*/
|
|
66
|
+
torus(majorRadius?: number, minorRadius?: number): this;
|
|
67
|
+
/**
|
|
68
|
+
* Excludes toroidal faces, optionally matching major and/or minor radius.
|
|
69
|
+
* @param majorRadius - Optional radius from the torus axis to the tube center.
|
|
70
|
+
* @param minorRadius - Optional radius of the tube itself.
|
|
71
|
+
*/
|
|
72
|
+
notTorus(majorRadius?: number, minorRadius?: number): this;
|
|
61
73
|
/**
|
|
62
74
|
* Selects conical faces.
|
|
63
75
|
*/
|
|
@@ -4,6 +4,7 @@ import { CircleFilter, NotCircleFilter } from "./circle-filter.js";
|
|
|
4
4
|
import { ConeFilter, NotConeFilter } from "./cone-filter.js";
|
|
5
5
|
import { CylinderCurveFilter, NotCylinderCurveFilter } from "./cylinder-curve.js";
|
|
6
6
|
import { CylinderFilter, NotCylinderFilter } from "./cylinder.js";
|
|
7
|
+
import { TorusFilter, NotTorusFilter } from "./torus-filter.js";
|
|
7
8
|
import { NotOnPlaneFilter, OnPlaneFilter } from "./on-plane.js";
|
|
8
9
|
import { NotParallelFilter, ParallelFilter } from "./parallel.js";
|
|
9
10
|
import { PlaneObject } from "../../features/plane.js";
|
|
@@ -182,6 +183,26 @@ export class FaceFilterBuilder extends FilterBuilderBase {
|
|
|
182
183
|
this.filters.push(filter);
|
|
183
184
|
return this;
|
|
184
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Selects toroidal faces, optionally matching major and/or minor radius.
|
|
188
|
+
* @param majorRadius - Optional radius from the torus axis to the tube center.
|
|
189
|
+
* @param minorRadius - Optional radius of the tube itself.
|
|
190
|
+
*/
|
|
191
|
+
torus(majorRadius, minorRadius) {
|
|
192
|
+
const filter = new TorusFilter(majorRadius, minorRadius);
|
|
193
|
+
this.filters.push(filter);
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Excludes toroidal faces, optionally matching major and/or minor radius.
|
|
198
|
+
* @param majorRadius - Optional radius from the torus axis to the tube center.
|
|
199
|
+
* @param minorRadius - Optional radius of the tube itself.
|
|
200
|
+
*/
|
|
201
|
+
notTorus(majorRadius, minorRadius) {
|
|
202
|
+
const filter = new NotTorusFilter(majorRadius, minorRadius);
|
|
203
|
+
this.filters.push(filter);
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
185
206
|
/**
|
|
186
207
|
* Selects conical faces.
|
|
187
208
|
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Matrix4 } from "../../math/matrix4.js";
|
|
2
|
+
import { Face } from "../../common/shapes.js";
|
|
3
|
+
import { FilterBase } from "../filter-base.js";
|
|
4
|
+
export declare class TorusFilter extends FilterBase<Face> {
|
|
5
|
+
private majorRadius?;
|
|
6
|
+
private minorRadius?;
|
|
7
|
+
constructor(majorRadius?: number, minorRadius?: number);
|
|
8
|
+
match(shape: Face): boolean;
|
|
9
|
+
compareTo(other: TorusFilter): boolean;
|
|
10
|
+
transform(matrix: Matrix4): TorusFilter;
|
|
11
|
+
}
|
|
12
|
+
export declare class NotTorusFilter extends FilterBase<Face> {
|
|
13
|
+
private majorRadius?;
|
|
14
|
+
private minorRadius?;
|
|
15
|
+
constructor(majorRadius?: number, minorRadius?: number);
|
|
16
|
+
match(shape: Face): boolean;
|
|
17
|
+
compareTo(other: NotTorusFilter): boolean;
|
|
18
|
+
transform(matrix: Matrix4): NotTorusFilter;
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { FilterBase } from "../filter-base.js";
|
|
2
|
+
import { FaceQuery } from "../../oc/face-query.js";
|
|
3
|
+
export class TorusFilter extends FilterBase {
|
|
4
|
+
majorRadius;
|
|
5
|
+
minorRadius;
|
|
6
|
+
constructor(majorRadius, minorRadius) {
|
|
7
|
+
super();
|
|
8
|
+
this.majorRadius = majorRadius;
|
|
9
|
+
this.minorRadius = minorRadius;
|
|
10
|
+
}
|
|
11
|
+
match(shape) {
|
|
12
|
+
return FaceQuery.isTorusFace(shape, this.majorRadius, this.minorRadius);
|
|
13
|
+
}
|
|
14
|
+
compareTo(other) {
|
|
15
|
+
return this.majorRadius === other.majorRadius && this.minorRadius === other.minorRadius;
|
|
16
|
+
}
|
|
17
|
+
transform(matrix) {
|
|
18
|
+
return new TorusFilter(this.majorRadius, this.minorRadius);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class NotTorusFilter extends FilterBase {
|
|
22
|
+
majorRadius;
|
|
23
|
+
minorRadius;
|
|
24
|
+
constructor(majorRadius, minorRadius) {
|
|
25
|
+
super();
|
|
26
|
+
this.majorRadius = majorRadius;
|
|
27
|
+
this.minorRadius = minorRadius;
|
|
28
|
+
}
|
|
29
|
+
match(shape) {
|
|
30
|
+
return !FaceQuery.isTorusFace(shape, this.majorRadius, this.minorRadius);
|
|
31
|
+
}
|
|
32
|
+
compareTo(other) {
|
|
33
|
+
return this.majorRadius === other.majorRadius && this.minorRadius === other.minorRadius;
|
|
34
|
+
}
|
|
35
|
+
transform(matrix) {
|
|
36
|
+
return new NotTorusFilter(this.majorRadius, this.minorRadius);
|
|
37
|
+
}
|
|
38
|
+
}
|