fluidcad 0.0.27 → 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/solid.js +5 -1
- package/lib/dist/features/chamfer.js +12 -6
- package/lib/dist/features/extrude-base.d.ts +36 -0
- package/lib/dist/features/extrude-base.js +105 -33
- package/lib/dist/features/extrude-to-face.js +13 -2
- package/lib/dist/features/extrude-two-distances.js +18 -3
- package/lib/dist/features/extrude.js +29 -6
- 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/revolve.js +13 -2
- package/lib/dist/features/sweep.js +13 -2
- package/lib/dist/helpers/scene-helpers.d.ts +7 -1
- package/lib/dist/helpers/scene-helpers.js +274 -9
- package/lib/dist/oc/boolean-ops.d.ts +29 -3
- package/lib/dist/oc/boolean-ops.js +107 -9
- 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/fillet-ops.d.ts +5 -3
- package/lib/dist/oc/fillet-ops.js +23 -4
- package/lib/dist/oc/intersection.js +6 -3
- package/lib/dist/oc/mesh.js +1 -1
- package/lib/dist/oc/shape-ops.d.ts +25 -0
- package/lib/dist/oc/shape-ops.js +131 -12
- 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/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/subtract.test.js +21 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/ui/dist/assets/{index-55iqIwnj.js → index-BrW_x4uc.js} +1 -1
- package/ui/dist/index.html +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Face } from "../common/face.js";
|
|
2
|
+
import { Edge } from "../common/edge.js";
|
|
2
3
|
import { SceneObject } from "../common/scene-object.js";
|
|
3
4
|
import { LazySelectionSceneObject } from "./lazy-scene-object.js";
|
|
4
5
|
import { normalizePoint2D } from "../helpers/normalize.js";
|
|
@@ -8,6 +9,18 @@ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
|
|
|
8
9
|
import { EdgeFilterBuilder } from "../filters/edge/edge-filter.js";
|
|
9
10
|
import { ShapeFilter } from "../filters/filter.js";
|
|
10
11
|
import { EdgeOps } from "../oc/edge-ops.js";
|
|
12
|
+
import { Explorer } from "../oc/explorer.js";
|
|
13
|
+
import { getOC } from "../oc/init.js";
|
|
14
|
+
import { ShapeHistoryTracker } from "../common/shape-history-tracker.js";
|
|
15
|
+
function dedupEdgesByIsSame(edges) {
|
|
16
|
+
const result = [];
|
|
17
|
+
for (const edge of edges) {
|
|
18
|
+
if (!result.some(r => r.getShape().IsSame(edge.getShape()))) {
|
|
19
|
+
result.push(edge);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
11
24
|
export class ExtrudeBase extends SceneObject {
|
|
12
25
|
_extrudable = null;
|
|
13
26
|
_faceSource = null;
|
|
@@ -80,19 +93,10 @@ export class ExtrudeBase extends SceneObject {
|
|
|
80
93
|
startEdges(...args) {
|
|
81
94
|
const suffix = this.buildSuffix('start-edges', args);
|
|
82
95
|
return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
|
|
83
|
-
|
|
84
|
-
const edges = parent.getState('start-edges') || [];
|
|
85
|
-
const transform = parent.getTransform();
|
|
86
|
-
const originalEdges = transform
|
|
87
|
-
? (this.getState('start-edges') || [])
|
|
88
|
-
: null;
|
|
89
|
-
return this.resolveEdges(edges, args, transform, originalEdges);
|
|
90
|
-
}
|
|
91
|
-
const faces = parent.getState('start-faces') || [];
|
|
92
|
-
const edges = faces.flatMap(f => f.getEdges());
|
|
96
|
+
const edges = this.getClassifiedEdges(parent, 'start-edges', 'start-faces');
|
|
93
97
|
const transform = parent.getTransform();
|
|
94
98
|
const originalEdges = transform
|
|
95
|
-
?
|
|
99
|
+
? this.getClassifiedEdges(this, 'start-edges', 'start-faces')
|
|
96
100
|
: null;
|
|
97
101
|
return this.resolveEdges(edges, args, transform, originalEdges);
|
|
98
102
|
}, this);
|
|
@@ -100,19 +104,10 @@ export class ExtrudeBase extends SceneObject {
|
|
|
100
104
|
endEdges(...args) {
|
|
101
105
|
const suffix = this.buildSuffix('end-edges', args);
|
|
102
106
|
return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
|
|
103
|
-
|
|
104
|
-
const edges = parent.getState('end-edges') || [];
|
|
105
|
-
const transform = parent.getTransform();
|
|
106
|
-
const originalEdges = transform
|
|
107
|
-
? (this.getState('end-edges') || [])
|
|
108
|
-
: null;
|
|
109
|
-
return this.resolveEdges(edges, args, transform, originalEdges);
|
|
110
|
-
}
|
|
111
|
-
const faces = parent.getState('end-faces') || [];
|
|
112
|
-
const edges = faces.flatMap(f => f.getEdges());
|
|
107
|
+
const edges = this.getClassifiedEdges(parent, 'end-edges', 'end-faces');
|
|
113
108
|
const transform = parent.getTransform();
|
|
114
109
|
const originalEdges = transform
|
|
115
|
-
?
|
|
110
|
+
? this.getClassifiedEdges(this, 'end-edges', 'end-faces')
|
|
116
111
|
: null;
|
|
117
112
|
return this.resolveEdges(edges, args, transform, originalEdges);
|
|
118
113
|
}, this);
|
|
@@ -131,13 +126,17 @@ export class ExtrudeBase extends SceneObject {
|
|
|
131
126
|
sideEdges(...args) {
|
|
132
127
|
const suffix = this.buildSuffix('side-edges', args);
|
|
133
128
|
return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
|
|
129
|
+
const classified = parent.getState('side-edges');
|
|
130
|
+
if (classified !== undefined) {
|
|
131
|
+
return this.resolveEdges(classified, args);
|
|
132
|
+
}
|
|
133
|
+
// Fallback for peer ops that haven't called classifyExtrudeEdges: derive on the fly.
|
|
134
134
|
const sideFaces = parent.getState('side-faces') || [];
|
|
135
135
|
const startFaces = parent.getState('start-faces') || [];
|
|
136
136
|
const endFaces = parent.getState('end-faces') || [];
|
|
137
137
|
const excludedEdges = [...startFaces, ...endFaces].flatMap(f => f.getEdges());
|
|
138
|
-
const edges = sideFaces.flatMap(f => f.getEdges())
|
|
139
|
-
.filter(e => !excludedEdges.some(ex => e.getShape().IsSame(ex.getShape())))
|
|
140
|
-
.filter((e, i, arr) => arr.findIndex(o => o.getShape().IsSame(e.getShape())) === i);
|
|
138
|
+
const edges = dedupEdgesByIsSame(sideFaces.flatMap(f => f.getEdges())
|
|
139
|
+
.filter(e => !excludedEdges.some(ex => e.getShape().IsSame(ex.getShape()))));
|
|
141
140
|
return this.resolveEdges(edges, args);
|
|
142
141
|
}, this);
|
|
143
142
|
}
|
|
@@ -169,12 +168,7 @@ export class ExtrudeBase extends SceneObject {
|
|
|
169
168
|
internalEdges(...args) {
|
|
170
169
|
const suffix = this.buildSuffix('internal-edges', args);
|
|
171
170
|
return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
|
|
172
|
-
|
|
173
|
-
const edges = parent.getState('internal-edges') || [];
|
|
174
|
-
return this.resolveEdges(edges, args);
|
|
175
|
-
}
|
|
176
|
-
const faces = parent.getState('internal-faces') || [];
|
|
177
|
-
const edges = faces.flatMap(f => f.getEdges());
|
|
171
|
+
const edges = this.getClassifiedEdges(parent, 'internal-edges', 'internal-faces');
|
|
178
172
|
return this.resolveEdges(edges, args);
|
|
179
173
|
}, this);
|
|
180
174
|
}
|
|
@@ -192,11 +186,89 @@ export class ExtrudeBase extends SceneObject {
|
|
|
192
186
|
capEdges(...args) {
|
|
193
187
|
const suffix = this.buildSuffix('cap-edges', args);
|
|
194
188
|
return new LazySelectionSceneObject(`${this.generateUniqueName(suffix)}`, (parent) => {
|
|
195
|
-
const
|
|
196
|
-
const edges = faces.flatMap(f => f.getEdges());
|
|
189
|
+
const edges = this.getClassifiedEdges(parent, 'cap-edges', 'cap-faces');
|
|
197
190
|
return this.resolveEdges(edges, args);
|
|
198
191
|
}, this);
|
|
199
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Read edges for a classification category, preferring the pre-computed
|
|
195
|
+
* state key (set by `classifyExtrudeEdges` during build) and falling back
|
|
196
|
+
* to deriving from the corresponding face-category state (for peer ops that
|
|
197
|
+
* haven't opted into the unified classification step yet).
|
|
198
|
+
*/
|
|
199
|
+
getClassifiedEdges(source, edgeKey, faceKey) {
|
|
200
|
+
const classified = source.getState(edgeKey);
|
|
201
|
+
if (classified !== undefined) {
|
|
202
|
+
return classified;
|
|
203
|
+
}
|
|
204
|
+
const faces = source.getState(faceKey) || [];
|
|
205
|
+
return dedupEdgesByIsSame(faces.flatMap(f => f.getEdges()));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Remap the state-stored face category arrays through a fusion's tool-side
|
|
209
|
+
* history so each face reference points at the actual post-fusion face in
|
|
210
|
+
* the final solid. Call after `fuseWithSceneObjects` returns a `toolHistory`.
|
|
211
|
+
*/
|
|
212
|
+
remapClassifiedFaces(history) {
|
|
213
|
+
const keys = ['start-faces', 'end-faces', 'side-faces', 'internal-faces', 'cap-faces'];
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
const faces = this.getState(key);
|
|
216
|
+
if (faces && faces.length > 0) {
|
|
217
|
+
this.setState(key, ShapeHistoryTracker.remapFaces(faces, history));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Record every face/edge of the given shapes as additions on this operation.
|
|
223
|
+
* Used by 3D ops in the "no scene fusion" path — when the tools land
|
|
224
|
+
* unchanged in the scene, every face/edge is brand new from this op's POV.
|
|
225
|
+
*/
|
|
226
|
+
recordShapeFacesAndEdgesAsAdditions(shapes) {
|
|
227
|
+
const oc = getOC();
|
|
228
|
+
const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
|
|
229
|
+
const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
|
|
230
|
+
for (const shape of shapes) {
|
|
231
|
+
for (const raw of Explorer.findShapes(shape.getShape(), FACE)) {
|
|
232
|
+
this.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), this);
|
|
233
|
+
}
|
|
234
|
+
for (const raw of Explorer.findShapes(shape.getShape(), EDGE)) {
|
|
235
|
+
this.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), this);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* One-shot edge classification: derive start/end/side/internal/cap edges
|
|
241
|
+
* from the already-classified face arrays in state and store them as
|
|
242
|
+
* `start-edges`, `end-edges`, `side-edges`, `internal-edges`, `cap-edges`.
|
|
243
|
+
*
|
|
244
|
+
* Call this once after face classification (and after any post-fusion
|
|
245
|
+
* face remapping) so that the selection accessors can just read the
|
|
246
|
+
* pre-computed arrays instead of re-deriving on every access. Matches
|
|
247
|
+
* the classification step from the spec: "Classify the new edges and
|
|
248
|
+
* faces created by the operation".
|
|
249
|
+
*
|
|
250
|
+
* Side edges are the edges of side faces minus any edge that's also on
|
|
251
|
+
* a start/end face (those already belong to start-edges / end-edges).
|
|
252
|
+
*/
|
|
253
|
+
classifyExtrudeEdges() {
|
|
254
|
+
const startFaces = this.getState('start-faces') || [];
|
|
255
|
+
const endFaces = this.getState('end-faces') || [];
|
|
256
|
+
const sideFaces = this.getState('side-faces') || [];
|
|
257
|
+
const internalFaces = this.getState('internal-faces') || [];
|
|
258
|
+
const capFaces = this.getState('cap-faces') || [];
|
|
259
|
+
const startEdges = dedupEdgesByIsSame(startFaces.flatMap(f => f.getEdges()));
|
|
260
|
+
const endEdges = dedupEdgesByIsSame(endFaces.flatMap(f => f.getEdges()));
|
|
261
|
+
const excludedForSide = [...startEdges, ...endEdges];
|
|
262
|
+
const sideEdges = dedupEdgesByIsSame(sideFaces.flatMap(f => f.getEdges())
|
|
263
|
+
.filter(e => !excludedForSide.some(ex => ex.getShape().IsSame(e.getShape()))));
|
|
264
|
+
const internalEdges = dedupEdgesByIsSame(internalFaces.flatMap(f => f.getEdges()));
|
|
265
|
+
const capEdges = dedupEdgesByIsSame(capFaces.flatMap(f => f.getEdges()));
|
|
266
|
+
this.setState('start-edges', startEdges);
|
|
267
|
+
this.setState('end-edges', endEdges);
|
|
268
|
+
this.setState('side-edges', sideEdges);
|
|
269
|
+
this.setState('internal-edges', internalEdges);
|
|
270
|
+
this.setState('cap-edges', capEdges);
|
|
271
|
+
}
|
|
200
272
|
buildSuffix(prefix, args) {
|
|
201
273
|
if (args.length === 0) {
|
|
202
274
|
return prefix;
|
|
@@ -91,20 +91,31 @@ export class ExtrudeToFace extends ExtrudeBase {
|
|
|
91
91
|
}
|
|
92
92
|
if (this._operationMode === 'remove') {
|
|
93
93
|
const scope = this.resolveFusionScope(allSceneObjects);
|
|
94
|
-
cutWithSceneObjects(scope, solids, plane, 0, this);
|
|
94
|
+
cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
|
|
95
|
+
this.setFinalShapes(this.getShapes());
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
if (sceneObjects.length === 0) {
|
|
98
99
|
this.addShapes(solids);
|
|
100
|
+
this.recordShapeFacesAndEdgesAsAdditions(solids);
|
|
101
|
+
this.classifyExtrudeEdges();
|
|
102
|
+
this.setFinalShapes(this.getShapes());
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, solids
|
|
105
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
|
|
106
|
+
recordHistoryFor: this,
|
|
107
|
+
});
|
|
102
108
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
103
109
|
if (modifiedShape.object) {
|
|
104
110
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
this.addShapes(fusionResult.newShapes);
|
|
114
|
+
if (fusionResult.toolHistory) {
|
|
115
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
116
|
+
}
|
|
117
|
+
this.classifyExtrudeEdges();
|
|
118
|
+
this.setFinalShapes(this.getShapes());
|
|
108
119
|
}
|
|
109
120
|
createAdvancedExtrude(sourceFace, targetFace, isPlanar, sketchPlane) {
|
|
110
121
|
const targetDistance = this.computeSignedDistanceToFace(targetFace, sketchPlane);
|
|
@@ -49,7 +49,9 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
|
|
|
49
49
|
const extrusions2 = extruder2.extrude();
|
|
50
50
|
const endFaces = extruder2.getEndFaces();
|
|
51
51
|
const all = [...extrusions1, ...extrusions2];
|
|
52
|
-
const
|
|
52
|
+
const halvesFuse = BooleanOps.fuse(all);
|
|
53
|
+
const extrusions = halvesFuse.result;
|
|
54
|
+
halvesFuse.dispose();
|
|
53
55
|
const remainingFaces = [];
|
|
54
56
|
const fusedStartFaces = [];
|
|
55
57
|
const fusedEndFaces = [];
|
|
@@ -121,14 +123,22 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
|
|
|
121
123
|
this.getSource()?.removeShapes(this);
|
|
122
124
|
if (this._operationMode === 'remove') {
|
|
123
125
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
124
|
-
cutWithSceneObjects(scope, extrusions, plane, this.distance1 + this.distance2, this
|
|
126
|
+
cutWithSceneObjects(scope, extrusions, plane, this.distance1 + this.distance2, this, {
|
|
127
|
+
recordHistoryFor: this,
|
|
128
|
+
});
|
|
129
|
+
this.setFinalShapes(this.getShapes());
|
|
125
130
|
return;
|
|
126
131
|
}
|
|
127
132
|
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
128
133
|
this.addShapes(extrusions);
|
|
134
|
+
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
135
|
+
this.classifyExtrudeEdges();
|
|
136
|
+
this.setFinalShapes(this.getShapes());
|
|
129
137
|
return;
|
|
130
138
|
}
|
|
131
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions
|
|
139
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
140
|
+
recordHistoryFor: this,
|
|
141
|
+
});
|
|
132
142
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
133
143
|
if (!modifiedShape.object) {
|
|
134
144
|
continue;
|
|
@@ -136,6 +146,11 @@ export class ExtrudeTwoDistances extends ExtrudeBase {
|
|
|
136
146
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
137
147
|
}
|
|
138
148
|
this.addShapes(fusionResult.newShapes);
|
|
149
|
+
if (fusionResult.toolHistory) {
|
|
150
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
151
|
+
}
|
|
152
|
+
this.classifyExtrudeEdges();
|
|
153
|
+
this.setFinalShapes(this.getShapes());
|
|
139
154
|
}
|
|
140
155
|
getDependencies() {
|
|
141
156
|
const source = this.getSource();
|
|
@@ -53,6 +53,7 @@ export class Extrude extends ExtrudeBase {
|
|
|
53
53
|
else {
|
|
54
54
|
this.buildAdd(faces, plane, context, inwardEdges, outwardEdges);
|
|
55
55
|
}
|
|
56
|
+
this.setFinalShapes(this.getShapes());
|
|
56
57
|
console.log(`[perf] Extrude.build TOTAL: ${(performance.now() - tBuild).toFixed(1)} ms`);
|
|
57
58
|
}
|
|
58
59
|
buildAdd(faces, plane, context, inwardEdges, outwardEdges) {
|
|
@@ -81,10 +82,15 @@ export class Extrude extends ExtrudeBase {
|
|
|
81
82
|
console.log("Extrusions before fusion:", extrusions.length);
|
|
82
83
|
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
83
84
|
this.addShapes(extrusions);
|
|
85
|
+
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
86
|
+
this.classifyExtrudeEdges();
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
const tFuse = performance.now();
|
|
87
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions,
|
|
90
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
91
|
+
glue: this.isFaceSourced() ? 'full' : undefined,
|
|
92
|
+
recordHistoryFor: this,
|
|
93
|
+
});
|
|
88
94
|
console.log(`[perf] Extrude.buildAdd.fuseWithSceneObjects: ${(performance.now() - tFuse).toFixed(1)} ms`);
|
|
89
95
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
90
96
|
if (!modifiedShape.object) {
|
|
@@ -93,6 +99,10 @@ export class Extrude extends ExtrudeBase {
|
|
|
93
99
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
94
100
|
}
|
|
95
101
|
this.addShapes(fusionResult.newShapes);
|
|
102
|
+
if (fusionResult.toolHistory) {
|
|
103
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
104
|
+
}
|
|
105
|
+
this.classifyExtrudeEdges();
|
|
96
106
|
}
|
|
97
107
|
buildSymmetric(faces, plane, context, inwardEdges, outwardEdges) {
|
|
98
108
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
@@ -103,7 +113,9 @@ export class Extrude extends ExtrudeBase {
|
|
|
103
113
|
const extrusions2 = extruder2.extrude();
|
|
104
114
|
const endFaces = extruder2.getEndFaces();
|
|
105
115
|
const all = [...extrusions1, ...extrusions2];
|
|
106
|
-
const
|
|
116
|
+
const halvesFuse = BooleanOps.fuse(all);
|
|
117
|
+
const extrusions = halvesFuse.result;
|
|
118
|
+
halvesFuse.dispose();
|
|
107
119
|
// Collect remaining faces and fused start/end faces from the fused solid.
|
|
108
120
|
// We need the fused face objects (not pre-fusion) for IsPartner matching.
|
|
109
121
|
const remainingFaces = [];
|
|
@@ -182,9 +194,13 @@ export class Extrude extends ExtrudeBase {
|
|
|
182
194
|
this.getSource()?.removeShapes(this);
|
|
183
195
|
if (extrusions.length === 0 || sceneObjects.length === 0) {
|
|
184
196
|
this.addShapes(extrusions);
|
|
197
|
+
this.recordShapeFacesAndEdgesAsAdditions(extrusions);
|
|
198
|
+
this.classifyExtrudeEdges();
|
|
185
199
|
return;
|
|
186
200
|
}
|
|
187
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions
|
|
201
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, extrusions, {
|
|
202
|
+
recordHistoryFor: this,
|
|
203
|
+
});
|
|
188
204
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
189
205
|
if (!modifiedShape.object) {
|
|
190
206
|
continue;
|
|
@@ -192,6 +208,10 @@ export class Extrude extends ExtrudeBase {
|
|
|
192
208
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
193
209
|
}
|
|
194
210
|
this.addShapes(fusionResult.newShapes);
|
|
211
|
+
if (fusionResult.toolHistory) {
|
|
212
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
213
|
+
}
|
|
214
|
+
this.classifyExtrudeEdges();
|
|
195
215
|
}
|
|
196
216
|
buildRemove(faces, plane, context) {
|
|
197
217
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
@@ -212,8 +232,9 @@ export class Extrude extends ExtrudeBase {
|
|
|
212
232
|
const extruder2 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset());
|
|
213
233
|
const extrusions2 = extruder2.extrude();
|
|
214
234
|
const all = [...extrusions1, ...extrusions2];
|
|
215
|
-
const
|
|
216
|
-
toolShapes = result;
|
|
235
|
+
const halvesFuse = BooleanOps.fuse(all);
|
|
236
|
+
toolShapes = halvesFuse.result;
|
|
237
|
+
halvesFuse.dispose();
|
|
217
238
|
}
|
|
218
239
|
}
|
|
219
240
|
else if (isThroughAll) {
|
|
@@ -229,7 +250,9 @@ export class Extrude extends ExtrudeBase {
|
|
|
229
250
|
toolShapes = extruder.extrude();
|
|
230
251
|
}
|
|
231
252
|
this.getSource()?.removeShapes(this);
|
|
232
|
-
cutWithSceneObjects(scope, toolShapes, plane, this.distance, this
|
|
253
|
+
cutWithSceneObjects(scope, toolShapes, plane, this.distance, this, {
|
|
254
|
+
recordHistoryFor: this,
|
|
255
|
+
});
|
|
233
256
|
}
|
|
234
257
|
getDependencies() {
|
|
235
258
|
const source = this.getSource();
|
|
@@ -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;
|
|
@@ -115,21 +115,32 @@ export class Revolve extends ExtrudeBase {
|
|
|
115
115
|
this.axis.removeShapes(this);
|
|
116
116
|
if (this._operationMode === 'remove') {
|
|
117
117
|
const scope = this.resolveFusionScope(context.getSceneObjects());
|
|
118
|
-
cutWithSceneObjects(scope, solids, plane, 0, this);
|
|
118
|
+
cutWithSceneObjects(scope, solids, plane, 0, this, { recordHistoryFor: this });
|
|
119
|
+
this.setFinalShapes(this.getShapes());
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
122
|
const sceneObjects = this.resolveFusionScope(context.getSceneObjects());
|
|
122
123
|
if (sceneObjects.length === 0) {
|
|
123
124
|
this.addShapes(solids);
|
|
125
|
+
this.recordShapeFacesAndEdgesAsAdditions(solids);
|
|
126
|
+
this.classifyExtrudeEdges();
|
|
127
|
+
this.setFinalShapes(this.getShapes());
|
|
124
128
|
return;
|
|
125
129
|
}
|
|
126
|
-
const fusionResult = fuseWithSceneObjects(sceneObjects, solids
|
|
130
|
+
const fusionResult = fuseWithSceneObjects(sceneObjects, solids, {
|
|
131
|
+
recordHistoryFor: this,
|
|
132
|
+
});
|
|
127
133
|
for (const modifiedShape of fusionResult.modifiedShapes) {
|
|
128
134
|
if (modifiedShape.object) {
|
|
129
135
|
modifiedShape.object.removeShape(modifiedShape.shape, this);
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
this.addShapes(fusionResult.newShapes);
|
|
139
|
+
if (fusionResult.toolHistory) {
|
|
140
|
+
this.remapClassifiedFaces(fusionResult.toolHistory);
|
|
141
|
+
}
|
|
142
|
+
this.classifyExtrudeEdges();
|
|
143
|
+
this.setFinalShapes(this.getShapes());
|
|
133
144
|
}
|
|
134
145
|
getDependencies() {
|
|
135
146
|
return this.extrudable ? [this.extrudable] : [];
|
|
@@ -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 });
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { SceneObject } from "../common/scene-object.js";
|
|
2
2
|
import { Shape } from "../common/shapes.js";
|
|
3
3
|
import { Plane } from "../math/plane.js";
|
|
4
|
+
import { ShapeHistory } from "../common/shape-history-tracker.js";
|
|
4
5
|
export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[], opts?: {
|
|
5
6
|
glue?: 'full' | 'shift';
|
|
7
|
+
recordHistoryFor?: SceneObject;
|
|
6
8
|
}): {
|
|
7
9
|
newShapes: Shape<any>[];
|
|
8
10
|
modifiedShapes: any[];
|
|
11
|
+
toolHistory?: undefined;
|
|
9
12
|
} | {
|
|
10
13
|
newShapes: Shape<import("occjs-wrapper").TopoDS_Shape>[];
|
|
11
14
|
modifiedShapes: {
|
|
12
15
|
shape: Shape<any>;
|
|
13
16
|
object: SceneObject;
|
|
14
17
|
}[];
|
|
18
|
+
toolHistory: ShapeHistory;
|
|
15
19
|
};
|
|
16
|
-
export declare function cutWithSceneObjects(sceneObjects: SceneObject[], toolShapes: Shape[], plane: Plane, distance: number, caller: SceneObject
|
|
20
|
+
export declare function cutWithSceneObjects(sceneObjects: SceneObject[], toolShapes: Shape[], plane: Plane, distance: number, caller: SceneObject, options?: {
|
|
21
|
+
recordHistoryFor?: SceneObject;
|
|
22
|
+
}): {
|
|
17
23
|
cleanedShapes: Shape[];
|
|
18
24
|
stockShapes: Shape[];
|
|
19
25
|
};
|