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
|
@@ -1,17 +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
|
-
|
|
4
|
+
import { ShapeHistory } from "../common/shape-history-tracker.js";
|
|
5
|
+
export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[], opts?: {
|
|
6
|
+
glue?: 'full' | 'shift';
|
|
7
|
+
recordHistoryFor?: SceneObject;
|
|
8
|
+
}): {
|
|
5
9
|
newShapes: Shape<any>[];
|
|
6
10
|
modifiedShapes: any[];
|
|
11
|
+
toolHistory?: undefined;
|
|
7
12
|
} | {
|
|
8
13
|
newShapes: Shape<import("occjs-wrapper").TopoDS_Shape>[];
|
|
9
14
|
modifiedShapes: {
|
|
10
15
|
shape: Shape<any>;
|
|
11
16
|
object: SceneObject;
|
|
12
17
|
}[];
|
|
18
|
+
toolHistory: ShapeHistory;
|
|
13
19
|
};
|
|
14
|
-
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
|
+
}): {
|
|
15
23
|
cleanedShapes: Shape[];
|
|
16
24
|
stockShapes: Shape[];
|
|
17
25
|
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { BooleanOps } from "../oc/boolean-ops.js";
|
|
2
2
|
import { ShapeOps } from "../oc/shape-ops.js";
|
|
3
3
|
import { classifyCutResult } from "./cut-helpers.js";
|
|
4
|
-
|
|
4
|
+
import { ShapeHistoryTracker } from "../common/shape-history-tracker.js";
|
|
5
|
+
import { Explorer } from "../oc/explorer.js";
|
|
6
|
+
import { Face } from "../common/face.js";
|
|
7
|
+
import { Edge } from "../common/edge.js";
|
|
8
|
+
import { getOC } from "../oc/init.js";
|
|
9
|
+
import { ColorTransfer } from "../oc/color-transfer.js";
|
|
10
|
+
export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
|
|
5
11
|
const modified = [];
|
|
12
|
+
const tCollect = performance.now();
|
|
6
13
|
const objShapeMap = new Map();
|
|
7
14
|
for (const obj of sceneObjects) {
|
|
8
15
|
const shapes = obj.getShapes({}, 'solid');
|
|
@@ -11,14 +18,19 @@ export function fuseWithSceneObjects(sceneObjects, extrusions) {
|
|
|
11
18
|
}
|
|
12
19
|
}
|
|
13
20
|
let sceneShapes = Array.from(objShapeMap.keys());
|
|
14
|
-
console.log(
|
|
15
|
-
const
|
|
16
|
-
const { result, newShapes, modifiedShapes } = BooleanOps.
|
|
21
|
+
console.log(`[perf] fuseWithSceneObjects.collect (scenes=${sceneShapes.length}, extrusions=${extrusions.length}): ${(performance.now() - tCollect).toFixed(1)} ms`);
|
|
22
|
+
const tFuse = performance.now();
|
|
23
|
+
const { result, newShapes, modifiedShapes, maker, dispose } = BooleanOps.fuseStockAndTools(sceneShapes, extrusions, opts);
|
|
24
|
+
console.log(`[perf] fuseWithSceneObjects.BooleanOps.fuseStockAndTools (glue=${opts?.glue ?? 'off'}): ${(performance.now() - tFuse).toFixed(1)} ms`);
|
|
17
25
|
if (newShapes.length === 0 && modifiedShapes.length === 0) {
|
|
18
26
|
console.log("No fusions were made.");
|
|
27
|
+
dispose();
|
|
28
|
+
if (opts?.recordHistoryFor) {
|
|
29
|
+
recordShapesAsAdditions(opts.recordHistoryFor, extrusions);
|
|
30
|
+
}
|
|
19
31
|
return {
|
|
20
32
|
newShapes: extrusions,
|
|
21
|
-
modifiedShapes: []
|
|
33
|
+
modifiedShapes: [],
|
|
22
34
|
};
|
|
23
35
|
}
|
|
24
36
|
for (const shape of modifiedShapes) {
|
|
@@ -30,9 +42,93 @@ export function fuseWithSceneObjects(sceneObjects, extrusions) {
|
|
|
30
42
|
// stay on their original owners so we must not duplicate them.
|
|
31
43
|
const unconsumed = sceneShapes.filter(s => !modifiedShapes.includes(s));
|
|
32
44
|
const shapesToAdd = result.filter(s => !unconsumed.some(u => u.getShape().IsPartner(s.getShape())));
|
|
33
|
-
|
|
45
|
+
let toolHistory;
|
|
46
|
+
if (opts?.recordHistoryFor) {
|
|
47
|
+
recordFusionHistory(opts.recordHistoryFor, sceneShapes, objShapeMap, shapesToAdd, maker);
|
|
48
|
+
// Separately track tool-side (extrusion) lineage so callers can remap
|
|
49
|
+
// pre-fusion categorizations (start/end/side/…) onto the post-fusion
|
|
50
|
+
// faces. We don't store these as modifications on any scene object —
|
|
51
|
+
// from the user's POV they are additions on the caller already.
|
|
52
|
+
toolHistory = ShapeHistoryTracker.collect(maker, extrusions);
|
|
53
|
+
}
|
|
54
|
+
dispose();
|
|
55
|
+
return { newShapes: shapesToAdd, modifiedShapes: modified, toolHistory };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Record faces/edges from each shape as additions on `caller`. Used when a
|
|
59
|
+
* fusion was a no-op and the new geometry is added to the scene unchanged.
|
|
60
|
+
*/
|
|
61
|
+
function recordShapesAsAdditions(caller, shapes) {
|
|
62
|
+
const oc = getOC();
|
|
63
|
+
const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
|
|
64
|
+
const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
|
|
65
|
+
for (const shape of shapes) {
|
|
66
|
+
for (const raw of Explorer.findShapes(shape.getShape(), FACE)) {
|
|
67
|
+
caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
|
|
68
|
+
}
|
|
69
|
+
for (const raw of Explorer.findShapes(shape.getShape(), EDGE)) {
|
|
70
|
+
caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Record modifications/removals on each scene-object owner, and additions on
|
|
76
|
+
* the caller. Modifications are per-scene-shape so we can correctly attribute
|
|
77
|
+
* each source face/edge back to its owning SceneObject.
|
|
78
|
+
*
|
|
79
|
+
* Additions: any face/edge in a new result shape that isn't already the target
|
|
80
|
+
* of a scene-shape modification. This captures both extrusion-derived faces
|
|
81
|
+
* (which appear in the result via tool-side Modified()) and truly new faces.
|
|
82
|
+
*/
|
|
83
|
+
function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker) {
|
|
84
|
+
const oc = getOC();
|
|
85
|
+
const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
|
|
86
|
+
const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
|
|
87
|
+
const claimedFaces = new oc.TopTools_MapOfShape();
|
|
88
|
+
const claimedEdges = new oc.TopTools_MapOfShape();
|
|
89
|
+
for (const sceneShape of sceneShapes) {
|
|
90
|
+
const owner = owners.get(sceneShape);
|
|
91
|
+
if (!owner) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const history = ShapeHistoryTracker.collect(maker, [sceneShape]);
|
|
95
|
+
for (const record of history.modifiedFaces) {
|
|
96
|
+
owner.recordModifiedFaces(record.sources, record.results, caller);
|
|
97
|
+
for (const r of record.results) {
|
|
98
|
+
claimedFaces.Add(r.getShape());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const record of history.modifiedEdges) {
|
|
102
|
+
owner.recordModifiedEdges(record.sources, record.results, caller);
|
|
103
|
+
for (const r of record.results) {
|
|
104
|
+
claimedEdges.Add(r.getShape());
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const face of history.removedFaces) {
|
|
108
|
+
owner.recordRemovedFace(face, caller);
|
|
109
|
+
}
|
|
110
|
+
for (const edge of history.removedEdges) {
|
|
111
|
+
owner.recordRemovedEdge(edge, caller);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const newShape of newShapes) {
|
|
115
|
+
for (const raw of Explorer.findShapes(newShape.getShape(), FACE)) {
|
|
116
|
+
if (!claimedFaces.Contains(raw)) {
|
|
117
|
+
caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const raw of Explorer.findShapes(newShape.getShape(), EDGE)) {
|
|
121
|
+
if (!claimedEdges.Contains(raw)) {
|
|
122
|
+
caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
ColorTransfer.applyThroughMaker(sceneShapes, newShapes, maker);
|
|
127
|
+
ColorTransfer.applyBleeding(sceneShapes, newShapes, maker);
|
|
128
|
+
claimedFaces.delete();
|
|
129
|
+
claimedEdges.delete();
|
|
34
130
|
}
|
|
35
|
-
export function cutWithSceneObjects(sceneObjects, toolShapes, plane, distance, caller) {
|
|
131
|
+
export function cutWithSceneObjects(sceneObjects, toolShapes, plane, distance, caller, options) {
|
|
36
132
|
const sceneObjectMap = new Map();
|
|
37
133
|
for (const obj of sceneObjects) {
|
|
38
134
|
const shapes = obj.getShapes({}, 'solid');
|
|
@@ -50,18 +146,190 @@ export function cutWithSceneObjects(sceneObjects, toolShapes, plane, distance, c
|
|
|
50
146
|
const stock = Array.from(shapeObjectMap.keys());
|
|
51
147
|
const cutResult = BooleanOps.cutMultiShape(stock, toolShapes, plane, distance);
|
|
52
148
|
const cleanedShapes = [];
|
|
149
|
+
const cleanups = [];
|
|
53
150
|
for (const shape of stock) {
|
|
54
151
|
const list = cutResult.modified(shape);
|
|
55
152
|
if (list.length) {
|
|
56
153
|
for (const newShape of list) {
|
|
57
|
-
const
|
|
58
|
-
caller.addShape(
|
|
59
|
-
cleanedShapes.push(
|
|
154
|
+
const cleanup = ShapeOps.cleanShapeWithLineage(newShape);
|
|
155
|
+
caller.addShape(cleanup.shape);
|
|
156
|
+
cleanedShapes.push(cleanup.shape);
|
|
157
|
+
cleanups.push(cleanup);
|
|
60
158
|
}
|
|
61
159
|
const obj = shapeObjectMap.get(shape);
|
|
62
160
|
obj.removeShape(shape, caller);
|
|
63
161
|
}
|
|
64
162
|
}
|
|
163
|
+
if (options?.recordHistoryFor) {
|
|
164
|
+
recordCutHistory(options.recordHistoryFor, stock, shapeObjectMap, cleanedShapes, cutResult.maker, cleanups);
|
|
165
|
+
}
|
|
166
|
+
for (const cleanup of cleanups) {
|
|
167
|
+
cleanup.dispose();
|
|
168
|
+
}
|
|
169
|
+
cutResult.dispose();
|
|
65
170
|
classifyCutResult(caller, stock, cleanedShapes, plane, distance);
|
|
66
171
|
return { cleanedShapes, stockShapes: stock };
|
|
67
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Record per-owner modifications/removals and caller-side additions for a cut.
|
|
175
|
+
* Mirrors the fusion variant but works with `BRepAlgoAPI_Cut`'s `Modified()` /
|
|
176
|
+
* `IsDeleted()` semantics on stock faces and edges. Additions are the faces
|
|
177
|
+
* and edges on the cleaned result shapes that aren't targets of any stock
|
|
178
|
+
* modification — that covers the section faces/edges created by the cut plus
|
|
179
|
+
* any tool-derived geometry that ended up in the result.
|
|
180
|
+
*/
|
|
181
|
+
function recordCutHistory(caller, stock, owners, cleanedShapes, maker, cleanups) {
|
|
182
|
+
const oc = getOC();
|
|
183
|
+
const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
|
|
184
|
+
const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
|
|
185
|
+
const claimedFaces = new oc.TopTools_MapOfShape();
|
|
186
|
+
const claimedEdges = new oc.TopTools_MapOfShape();
|
|
187
|
+
// Remap pre-clean faces through the UnifySameDomain history of whichever
|
|
188
|
+
// cleanup handled them. Returns flattened post-clean faces.
|
|
189
|
+
const remapPreCleanFaces = (preCleanFaces) => {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const face of preCleanFaces) {
|
|
192
|
+
let matched = false;
|
|
193
|
+
for (const cleanup of cleanups) {
|
|
194
|
+
const remapped = cleanup.remapFace(face);
|
|
195
|
+
if (remapped !== null) {
|
|
196
|
+
out.push(...remapped);
|
|
197
|
+
matched = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!matched) {
|
|
202
|
+
// No cleanup claimed this face — it's somehow outside the cleaned
|
|
203
|
+
// solids. Fall through as-is.
|
|
204
|
+
out.push(face);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
};
|
|
209
|
+
const remapPreCleanEdges = (preCleanEdges) => {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const edge of preCleanEdges) {
|
|
212
|
+
let matched = false;
|
|
213
|
+
for (const cleanup of cleanups) {
|
|
214
|
+
const remapped = cleanup.remapEdge(edge);
|
|
215
|
+
if (remapped !== null) {
|
|
216
|
+
out.push(...remapped);
|
|
217
|
+
matched = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!matched) {
|
|
222
|
+
out.push(edge);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
};
|
|
227
|
+
for (const stockShape of stock) {
|
|
228
|
+
const owner = owners.get(stockShape);
|
|
229
|
+
if (!owner) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const history = ShapeHistoryTracker.collect(maker, [stockShape]);
|
|
233
|
+
for (const record of history.modifiedFaces) {
|
|
234
|
+
const postCleanResults = remapPreCleanFaces(record.results);
|
|
235
|
+
if (postCleanResults.length === 0) {
|
|
236
|
+
// Entire modification was removed by UnifySameDomain — record as removal instead.
|
|
237
|
+
for (const src of record.sources) {
|
|
238
|
+
owner.recordRemovedFace(src, caller);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
owner.recordModifiedFaces(record.sources, postCleanResults, caller);
|
|
243
|
+
for (const r of postCleanResults) {
|
|
244
|
+
claimedFaces.Add(r.getShape());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const record of history.modifiedEdges) {
|
|
248
|
+
const postCleanResults = remapPreCleanEdges(record.results);
|
|
249
|
+
if (postCleanResults.length === 0) {
|
|
250
|
+
for (const src of record.sources) {
|
|
251
|
+
owner.recordRemovedEdge(src, caller);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
owner.recordModifiedEdges(record.sources, postCleanResults, caller);
|
|
256
|
+
for (const r of postCleanResults) {
|
|
257
|
+
claimedEdges.Add(r.getShape());
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const face of history.removedFaces) {
|
|
261
|
+
owner.recordRemovedFace(face, caller);
|
|
262
|
+
}
|
|
263
|
+
for (const edge of history.removedEdges) {
|
|
264
|
+
owner.recordRemovedEdge(edge, caller);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const cleaned of cleanedShapes) {
|
|
268
|
+
for (const raw of Explorer.findShapes(cleaned.getShape(), FACE)) {
|
|
269
|
+
if (!claimedFaces.Contains(raw)) {
|
|
270
|
+
caller.recordAddedFace(Face.fromTopoDSFace(Explorer.toFace(raw)), caller);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const raw of Explorer.findShapes(cleaned.getShape(), EDGE)) {
|
|
274
|
+
if (!claimedEdges.Contains(raw)) {
|
|
275
|
+
caller.recordAddedEdge(Edge.fromTopoDSEdge(Explorer.toEdge(raw)), caller);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
propagateFaceColorsViaCut(stock, cleanedShapes, maker, cleanups);
|
|
280
|
+
claimedFaces.delete();
|
|
281
|
+
claimedEdges.delete();
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Cut-path color propagation. Chains through UnifySameDomain's history via
|
|
285
|
+
* the `cleanups[]` remapFace callbacks so colors land on the actual
|
|
286
|
+
* post-clean faces in `cleanedShapes`.
|
|
287
|
+
*/
|
|
288
|
+
function propagateFaceColorsViaCut(stock, cleanedShapes, maker, cleanups) {
|
|
289
|
+
const oc = getOC();
|
|
290
|
+
const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
|
|
291
|
+
for (const stockShape of stock) {
|
|
292
|
+
if (!stockShape.hasColors()) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
for (const entry of stockShape.colorMap) {
|
|
296
|
+
const modifiedRaws = ShapeOps.shapeListToArray(maker.Modified(entry.shape))
|
|
297
|
+
.filter(s => s.ShapeType() === FACE);
|
|
298
|
+
let preCleanFaces;
|
|
299
|
+
if (modifiedRaws.length > 0) {
|
|
300
|
+
preCleanFaces = modifiedRaws.map(r => Face.fromTopoDSFace(Explorer.toFace(r)));
|
|
301
|
+
}
|
|
302
|
+
else if (!maker.IsDeleted(entry.shape)) {
|
|
303
|
+
preCleanFaces = [Face.fromTopoDSFace(Explorer.toFace(entry.shape))];
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// Chain through each cleanup's UnifySameDomain lineage
|
|
309
|
+
const postCleanFaces = [];
|
|
310
|
+
for (const preFace of preCleanFaces) {
|
|
311
|
+
let matched = false;
|
|
312
|
+
for (const cleanup of cleanups) {
|
|
313
|
+
const remapped = cleanup.remapFace(preFace);
|
|
314
|
+
if (remapped !== null) {
|
|
315
|
+
postCleanFaces.push(...remapped);
|
|
316
|
+
matched = true;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!matched) {
|
|
321
|
+
postCleanFaces.push(preFace);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const postFace of postCleanFaces) {
|
|
325
|
+
for (const cleaned of cleanedShapes) {
|
|
326
|
+
const faces = Explorer.findShapes(cleaned.getShape(), FACE);
|
|
327
|
+
if (faces.some(f => f.IsSame(postFace.getShape()))) {
|
|
328
|
+
cleaned.setColor(postFace.getShape(), entry.color);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
package/lib/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function registerBuilder<T extends Function>(builder: (context: S
|
|
|
20
20
|
export declare function init(rootPath?: string): Promise<{
|
|
21
21
|
currentScene: Scene;
|
|
22
22
|
currentFile: string;
|
|
23
|
+
renderer: import("./rendering/render.js").SceneRenderer;
|
|
23
24
|
rootPath: string;
|
|
24
25
|
setCurrentFile(filePath: string): void;
|
|
25
26
|
startScene(): Scene;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TopoDS_Shape } from "occjs-wrapper";
|
|
1
|
+
import type { BRepAlgoAPI_Cut, TopoDS_Shape } from "occjs-wrapper";
|
|
2
2
|
import { Shape } from "../common/shape.js";
|
|
3
3
|
import { Solid } from "../common/solid.js";
|
|
4
4
|
import { Edge } from "../common/edge.js";
|
|
@@ -8,18 +8,46 @@ export declare class BooleanOps {
|
|
|
8
8
|
static cutShapes(shape: Shape, tool: Shape): Shape;
|
|
9
9
|
static cutShapesRaw(shape: TopoDS_Shape, tool: TopoDS_Shape): TopoDS_Shape;
|
|
10
10
|
static cutMultiShape(stocks: Shape[], tools: Shape[], plane?: Plane, cutDistance?: number): {
|
|
11
|
-
result: import("../common/wire.js").Wire
|
|
12
|
-
modified: (shape: Shape) => (import("../common/wire.js").Wire
|
|
11
|
+
result: Solid | Face | Edge | import("../common/wire.js").Wire;
|
|
12
|
+
modified: (shape: Shape) => (Solid | Face | Edge | import("../common/wire.js").Wire)[];
|
|
13
13
|
sectionEdges: Edge[];
|
|
14
14
|
startEdges: Edge[];
|
|
15
15
|
endEdges: Edge[];
|
|
16
16
|
internalEdges: Edge[];
|
|
17
17
|
internalFaces: Face[];
|
|
18
|
+
maker: BRepAlgoAPI_Cut;
|
|
19
|
+
dispose: () => void;
|
|
18
20
|
};
|
|
19
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Fuse with proper OpenCascade argument-vs-tool separation. Use this for
|
|
23
|
+
* operations that fuse newly built geometry into an existing scene (extrude,
|
|
24
|
+
* revolve, sweep, loft) where we need `maker.Modified(stockFace)` lineage to
|
|
25
|
+
* track which existing face became which result face.
|
|
26
|
+
*
|
|
27
|
+
* Returns the underlying `BRepAlgoAPI_Fuse` so the caller can query
|
|
28
|
+
* `Modified()` / `Generated()` / `IsDeleted()` for history tracking. The
|
|
29
|
+
* caller MUST invoke `dispose()` exactly once to release the maker.
|
|
30
|
+
*
|
|
31
|
+
* Do not use this for the user-facing `Fuse` scene object (keep
|
|
32
|
+
* `BooleanOps.fuse` for that — it treats all inputs as symmetric peers).
|
|
33
|
+
*/
|
|
34
|
+
static fuseStockAndTools(stock: Shape[], tools: Shape[], opts?: {
|
|
35
|
+
glue?: 'full' | 'shift';
|
|
36
|
+
}): {
|
|
20
37
|
result: Shape[];
|
|
21
38
|
modifiedShapes: Shape[];
|
|
22
39
|
newShapes: Shape[];
|
|
40
|
+
maker: any;
|
|
41
|
+
dispose: () => void;
|
|
42
|
+
};
|
|
43
|
+
static fuse(args: Shape[], opts?: {
|
|
44
|
+
glue?: 'full' | 'shift';
|
|
45
|
+
}): {
|
|
46
|
+
result: Shape[];
|
|
47
|
+
modifiedShapes: Shape[];
|
|
48
|
+
newShapes: Shape[];
|
|
49
|
+
maker: any;
|
|
50
|
+
dispose: () => void;
|
|
23
51
|
};
|
|
24
52
|
static fuseFaces(args: Shape[]): {
|
|
25
53
|
result: Shape[];
|
|
@@ -14,12 +14,19 @@ export class BooleanOps {
|
|
|
14
14
|
static cutShapesRaw(shape, tool) {
|
|
15
15
|
const oc = getOC();
|
|
16
16
|
const progress = new oc.Message_ProgressRange();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
let cutter;
|
|
18
|
+
try {
|
|
19
|
+
cutter = new oc.BRepAlgoAPI_Cut(shape, tool, progress);
|
|
20
|
+
cutter.Build(progress);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
progress.delete();
|
|
24
|
+
throw new Error("Cut failed");
|
|
25
|
+
}
|
|
26
|
+
if (!cutter.IsDone() || cutter.HasErrors()) {
|
|
20
27
|
cutter.delete();
|
|
21
28
|
progress.delete();
|
|
22
|
-
throw new Error("Cut
|
|
29
|
+
throw new Error("Cut failed");
|
|
23
30
|
}
|
|
24
31
|
const result = cutter.Shape();
|
|
25
32
|
cutter.delete();
|
|
@@ -113,17 +120,107 @@ export class BooleanOps {
|
|
|
113
120
|
.map(rf => Face.fromTopoDSFace(Explorer.toFace(rf)));
|
|
114
121
|
stockEdgeMap.delete();
|
|
115
122
|
stockFaceMap.delete();
|
|
116
|
-
progress.delete();
|
|
117
123
|
stockList.delete();
|
|
118
124
|
toolList.delete();
|
|
119
|
-
|
|
125
|
+
let disposed = false;
|
|
126
|
+
const dispose = () => {
|
|
127
|
+
if (disposed) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
disposed = true;
|
|
131
|
+
cutMaker.delete();
|
|
132
|
+
progress.delete();
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
result: wrappedResult,
|
|
136
|
+
modified,
|
|
137
|
+
sectionEdges,
|
|
138
|
+
startEdges,
|
|
139
|
+
endEdges,
|
|
140
|
+
internalEdges,
|
|
141
|
+
internalFaces,
|
|
142
|
+
maker: cutMaker,
|
|
143
|
+
dispose,
|
|
144
|
+
};
|
|
120
145
|
}
|
|
121
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Fuse with proper OpenCascade argument-vs-tool separation. Use this for
|
|
148
|
+
* operations that fuse newly built geometry into an existing scene (extrude,
|
|
149
|
+
* revolve, sweep, loft) where we need `maker.Modified(stockFace)` lineage to
|
|
150
|
+
* track which existing face became which result face.
|
|
151
|
+
*
|
|
152
|
+
* Returns the underlying `BRepAlgoAPI_Fuse` so the caller can query
|
|
153
|
+
* `Modified()` / `Generated()` / `IsDeleted()` for history tracking. The
|
|
154
|
+
* caller MUST invoke `dispose()` exactly once to release the maker.
|
|
155
|
+
*
|
|
156
|
+
* Do not use this for the user-facing `Fuse` scene object (keep
|
|
157
|
+
* `BooleanOps.fuse` for that — it treats all inputs as symmetric peers).
|
|
158
|
+
*/
|
|
159
|
+
static fuseStockAndTools(stock, tools, opts) {
|
|
122
160
|
const oc = getOC();
|
|
123
161
|
const builder = new oc.BRepAlgoAPI_Fuse();
|
|
124
162
|
builder.SetNonDestructive(true);
|
|
125
163
|
builder.SetCheckInverted(true);
|
|
126
164
|
builder.SetRunParallel(true);
|
|
165
|
+
if (opts?.glue === 'full') {
|
|
166
|
+
builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueFull);
|
|
167
|
+
}
|
|
168
|
+
else if (opts?.glue === 'shift') {
|
|
169
|
+
builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueShift);
|
|
170
|
+
}
|
|
171
|
+
const stockList = new oc.TopTools_ListOfShape();
|
|
172
|
+
for (const s of stock) {
|
|
173
|
+
stockList.Append(s.getShape());
|
|
174
|
+
}
|
|
175
|
+
const toolList = new oc.TopTools_ListOfShape();
|
|
176
|
+
for (const t of tools) {
|
|
177
|
+
toolList.Append(t.getShape());
|
|
178
|
+
}
|
|
179
|
+
builder.SetArguments(stockList);
|
|
180
|
+
builder.SetTools(toolList);
|
|
181
|
+
const progress = new oc.Message_ProgressRange();
|
|
182
|
+
builder.Build(progress);
|
|
183
|
+
builder.SimplifyResult(false, true, oc.Precision.Angular());
|
|
184
|
+
const resultShape = builder.Shape();
|
|
185
|
+
const rawShapes = Explorer.findAllShapes(resultShape);
|
|
186
|
+
const result = rawShapes.map(s => ShapeFactory.fromShape(s));
|
|
187
|
+
const allInputs = [...stock, ...tools];
|
|
188
|
+
const modifiedShapes = [];
|
|
189
|
+
for (const shape of allInputs) {
|
|
190
|
+
if (builder.IsDeleted(shape.getShape())) {
|
|
191
|
+
modifiedShapes.push(shape);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const newShapes = [];
|
|
195
|
+
for (const s of result) {
|
|
196
|
+
const existsInArgs = allInputs.some(arg => arg.getShape().IsPartner(s.getShape()));
|
|
197
|
+
if (!existsInArgs) {
|
|
198
|
+
newShapes.push(s);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let disposed = false;
|
|
202
|
+
const dispose = () => {
|
|
203
|
+
if (disposed) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
disposed = true;
|
|
207
|
+
builder.delete();
|
|
208
|
+
progress.delete();
|
|
209
|
+
};
|
|
210
|
+
return { result, newShapes, modifiedShapes, maker: builder, dispose };
|
|
211
|
+
}
|
|
212
|
+
static fuse(args, opts) {
|
|
213
|
+
const oc = getOC();
|
|
214
|
+
const builder = new oc.BRepAlgoAPI_Fuse();
|
|
215
|
+
builder.SetNonDestructive(true);
|
|
216
|
+
builder.SetCheckInverted(true);
|
|
217
|
+
builder.SetRunParallel(true);
|
|
218
|
+
if (opts?.glue === 'full') {
|
|
219
|
+
builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueFull);
|
|
220
|
+
}
|
|
221
|
+
else if (opts?.glue === 'shift') {
|
|
222
|
+
builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueShift);
|
|
223
|
+
}
|
|
127
224
|
const argsList = new oc.TopTools_ListOfShape();
|
|
128
225
|
for (const arg of args) {
|
|
129
226
|
argsList.Append(arg.getShape());
|
|
@@ -134,11 +231,16 @@ export class BooleanOps {
|
|
|
134
231
|
builder.SetArguments(list);
|
|
135
232
|
builder.SetTools(argsList);
|
|
136
233
|
const progress = new oc.Message_ProgressRange();
|
|
234
|
+
const tBuild = performance.now();
|
|
137
235
|
builder.Build(progress);
|
|
236
|
+
console.log(`[perf] BooleanOps.fuse.Build (args=${args.length}): ${(performance.now() - tBuild).toFixed(1)} ms`);
|
|
237
|
+
const tSimplify = performance.now();
|
|
138
238
|
builder.SimplifyResult(false, true, oc.Precision.Angular());
|
|
239
|
+
console.log(`[perf] BooleanOps.fuse.SimplifyResult: ${(performance.now() - tSimplify).toFixed(1)} ms`);
|
|
139
240
|
const resultShape = builder.Shape();
|
|
241
|
+
const tExplore = performance.now();
|
|
140
242
|
const rawShapes = Explorer.findAllShapes(resultShape);
|
|
141
|
-
console.log(
|
|
243
|
+
console.log(`[perf] BooleanOps.fuse.findAllShapes (count=${rawShapes.length}): ${(performance.now() - tExplore).toFixed(1)} ms`);
|
|
142
244
|
const result = rawShapes.map(s => ShapeFactory.fromShape(s));
|
|
143
245
|
const modifiedShapes = [];
|
|
144
246
|
for (const shape of args) {
|
|
@@ -146,16 +248,25 @@ export class BooleanOps {
|
|
|
146
248
|
modifiedShapes.push(shape);
|
|
147
249
|
}
|
|
148
250
|
}
|
|
149
|
-
builder.delete();
|
|
150
|
-
progress.delete();
|
|
151
251
|
const newShapes = [];
|
|
252
|
+
const tPartner = performance.now();
|
|
152
253
|
for (const s of result) {
|
|
153
254
|
const existsInArgs = args.some(arg => arg.getShape().IsPartner(s.getShape()));
|
|
154
255
|
if (!existsInArgs) {
|
|
155
256
|
newShapes.push(s);
|
|
156
257
|
}
|
|
157
258
|
}
|
|
158
|
-
|
|
259
|
+
console.log(`[perf] BooleanOps.fuse.IsPartner check (result=${result.length} x args=${args.length}): ${(performance.now() - tPartner).toFixed(1)} ms`);
|
|
260
|
+
let disposed = false;
|
|
261
|
+
const dispose = () => {
|
|
262
|
+
if (disposed) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
disposed = true;
|
|
266
|
+
builder.delete();
|
|
267
|
+
progress.delete();
|
|
268
|
+
};
|
|
269
|
+
return { result, newShapes, modifiedShapes, maker: builder, dispose };
|
|
159
270
|
}
|
|
160
271
|
static fuseFaces(args) {
|
|
161
272
|
const oc = getOC();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BRepBuilderAPI_MakeShape } from "occjs-wrapper";
|
|
2
|
+
import { Shape } from "../common/shape.js";
|
|
3
|
+
import type { CleanShapeLineage } from "./shape-ops.js";
|
|
4
|
+
/**
|
|
5
|
+
* Walk each source shape's `colorMap`, find where each colored face ended up in
|
|
6
|
+
* the result shapes via `maker.Modified()` (falling back to the unchanged face
|
|
7
|
+
* if `!IsDeleted`), and apply the color to whichever result shape now owns it.
|
|
8
|
+
*
|
|
9
|
+
* Works for any `BRepBuilderAPI_MakeShape`-derived maker — fuse, cut, fillet,
|
|
10
|
+
* chamfer, transform, etc.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ColorTransfer {
|
|
13
|
+
static applyThroughMaker(sources: Shape[], results: Shape[], maker: BRepBuilderAPI_MakeShape): void;
|
|
14
|
+
/**
|
|
15
|
+
* Color bleed pass: spreads colors to result faces that came from new
|
|
16
|
+
* geometry (tool inputs, generated faces, or just brand-new) by walking
|
|
17
|
+
* face-edge adjacency in each result solid.
|
|
18
|
+
*
|
|
19
|
+
* Faces that came from `sceneSources` (whether modified or unchanged) are
|
|
20
|
+
* NOT bled — those represent existing geometry whose color state the user
|
|
21
|
+
* explicitly chose. Faces NOT from any sceneSource are eligible: this
|
|
22
|
+
* covers tool extrusions, fillet/chamfer-generated surfaces, and cut
|
|
23
|
+
* section faces.
|
|
24
|
+
*
|
|
25
|
+
* Iterates until stable so newly-bled faces can spread color further.
|
|
26
|
+
* Call AFTER `applyThroughMaker` so the colored seeds are in place.
|
|
27
|
+
*/
|
|
28
|
+
static applyBleeding(sceneSources: Shape[], results: Shape[], maker: BRepBuilderAPI_MakeShape): void;
|
|
29
|
+
/**
|
|
30
|
+
* Transfer colors from a pre-clean source shape through a `cleanShapeWithLineage`
|
|
31
|
+
* cleanup's `BRepTools_History` onto the post-clean result. Use this when an
|
|
32
|
+
* op is chained as `maker → cleanShape` — first apply `applyThroughMaker` to
|
|
33
|
+
* move colors from the original source onto the pre-clean result, then call
|
|
34
|
+
* this to chain them through the cleanup's UnifySameDomain history.
|
|
35
|
+
*/
|
|
36
|
+
static applyThroughCleanup(source: Shape, cleanup: CleanShapeLineage): void;
|
|
37
|
+
}
|