brep-io-kernel 1.0.98 → 1.0.99
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/dist-kernel/brep-kernel.js +24918 -23977
- package/package.json +1 -1
- package/src/BREP/SolidMethods/booleanOps.js +20 -4
- package/src/UI/sketcher/SketchMode3D.js +157 -33
- package/src/UI/sketcher/dimensions.js +59 -0
- package/src/UI/sketcher/glyphs.js +31 -0
- package/src/UI/sketcher/highlights.js +3 -1
- package/src/features/hole/HoleFeature.js +264 -17
- package/src/features/sketch/SketchFeature.js +117 -12
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +42 -7
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +104 -0
- package/src/tests/fixtures/sketchSolverTopology/README.md +46 -0
- package/src/tests/fixtures/sketchSolverTopology/coincident_chain_fixture.json +48 -0
- package/src/tests/fixtures/sketchSolverTopology/rect_width_height_fixture.json +43 -0
- package/src/tests/fixtures/sketchSolverTopology/sketch_throttel_expression_sequence_fixture.json +25 -0
- package/src/tests/partFiles/sketch_throttel_testing.BREP.json +562 -0
- package/src/tests/sketchSolverTopologyFixtureLoader.js +308 -0
- package/src/tests/test_sketch_solver_topology_stability.js +348 -0
- package/src/tests/tests.js +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brep-io-kernel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.99",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "pnpm prepareFonts && pnpm generateLicenses && pnpm build:kernel && vite --host 0.0.0.0",
|
|
6
6
|
"build": "pnpm prepareFonts && pnpm generateLicenses && pnpm build:kernel && vite build",
|
|
@@ -13,7 +13,7 @@ export function _combineIdMaps(other) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function _collapseFaceIdsByName(solid) {
|
|
16
|
-
if (!solid || !solid._faceNameToID || !solid._idToFaceName || !Array.isArray(solid._triIDs)) return;
|
|
16
|
+
if (!solid || !solid._faceNameToID || !solid._idToFaceName || !Array.isArray(solid._triIDs)) return false;
|
|
17
17
|
const nameToId = solid._faceNameToID;
|
|
18
18
|
let changed = false;
|
|
19
19
|
|
|
@@ -28,7 +28,7 @@ function _collapseFaceIdsByName(solid) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
if (!changed) return;
|
|
31
|
+
if (!changed) return false;
|
|
32
32
|
|
|
33
33
|
solid._idToFaceName = new Map(
|
|
34
34
|
[...solid._faceNameToID.entries()].map(([name, id]) => [id, name]),
|
|
@@ -37,6 +37,7 @@ function _collapseFaceIdsByName(solid) {
|
|
|
37
37
|
solid._dirty = true;
|
|
38
38
|
try { if (solid._manifold && typeof solid._manifold.delete === 'function') solid._manifold.delete(); } catch { }
|
|
39
39
|
solid._manifold = null;
|
|
40
|
+
return true;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function baseSolidCtor(obj) {
|
|
@@ -111,6 +112,7 @@ export function setTolerance(tolerance) {
|
|
|
111
112
|
try { out._auxEdges = Array.isArray(this._auxEdges) ? this._auxEdges.slice() : []; } catch { }
|
|
112
113
|
try { out._faceMetadata = new Map(this._faceMetadata); } catch { }
|
|
113
114
|
try { out._edgeMetadata = new Map(this._edgeMetadata); } catch { }
|
|
115
|
+
_collapseFaceIdsByName(out);
|
|
114
116
|
return out;
|
|
115
117
|
}
|
|
116
118
|
export function simplify(tolerance = undefined, updateInPlace = false) {
|
|
@@ -171,10 +173,23 @@ export function simplify(tolerance = undefined, updateInPlace = false) {
|
|
|
171
173
|
try { if (meshOut && typeof meshOut.delete === 'function') meshOut.delete(); } catch { }
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
|
|
176
|
+
if (updateInPlace) {
|
|
177
|
+
_collapseFaceIdsByName(this);
|
|
178
|
+
this._manifoldize();
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const mapForReturn = new Map(this._idToFaceName);
|
|
183
|
+
|
|
184
|
+
// Detach this solid from `outM` before rebuilding a second solid from it.
|
|
185
|
+
// This avoids sharing/deleting one manifold object between two Solid instances.
|
|
186
|
+
this._manifold = null;
|
|
187
|
+
this._dirty = true;
|
|
188
|
+
this._faceIndex = null;
|
|
189
|
+
_collapseFaceIdsByName(this);
|
|
175
190
|
|
|
191
|
+
const returnObject = Solid._fromManifold(outM, mapForReturn);
|
|
176
192
|
this._manifoldize();
|
|
177
|
-
// Return the mutated Solid (chainable)
|
|
178
193
|
return returnObject;
|
|
179
194
|
}
|
|
180
195
|
|
|
@@ -226,5 +241,6 @@ export function _fromManifold(manifoldObj, idToFaceName) {
|
|
|
226
241
|
|
|
227
242
|
solid._manifold = manifoldObj;
|
|
228
243
|
solid._dirty = false;
|
|
244
|
+
_collapseFaceIdsByName(solid);
|
|
229
245
|
try { return solid; } finally { try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch { } }
|
|
230
246
|
}
|
|
@@ -54,6 +54,7 @@ export class SketchMode3D {
|
|
|
54
54
|
sy: 0,
|
|
55
55
|
start: { dx: 0, dy: 0 },
|
|
56
56
|
};
|
|
57
|
+
this._externalRefPointIds = new Set();
|
|
57
58
|
// Track SKETCH groups we hide while editing so we can restore visibility
|
|
58
59
|
this._hiddenSketches = [];
|
|
59
60
|
// No clipping plane; orientation must do the work
|
|
@@ -206,6 +207,8 @@ export class SketchMode3D {
|
|
|
206
207
|
} catch { }
|
|
207
208
|
},
|
|
208
209
|
});
|
|
210
|
+
this._solver.sketchObject = this.#normalizeSketchInput(this._solver.sketchObject);
|
|
211
|
+
this.#refreshExternalRefPointIdsCache();
|
|
209
212
|
|
|
210
213
|
// Initialize solver settings
|
|
211
214
|
this._solverSettings = {
|
|
@@ -501,6 +504,7 @@ export class SketchMode3D {
|
|
|
501
504
|
if (!this._solver) return false;
|
|
502
505
|
const normalized = this.#normalizeSketchInput(sketch);
|
|
503
506
|
this._solver.sketchObject = normalized;
|
|
507
|
+
this.#refreshExternalRefPointIdsCache();
|
|
504
508
|
try { this._solver.solveSketch("full"); } catch { }
|
|
505
509
|
this._selection.clear();
|
|
506
510
|
this.#rebuildSketchGraphics();
|
|
@@ -816,8 +820,7 @@ export class SketchMode3D {
|
|
|
816
820
|
}
|
|
817
821
|
// Prevent dragging of external reference points; allow selection only
|
|
818
822
|
try {
|
|
819
|
-
const
|
|
820
|
-
const isExternal = (f?.persistentData?.externalRefs || []).some((r) => r.p0 === hit || r.p1 === hit);
|
|
823
|
+
const isExternal = this.#isExternalRefPointId(hit);
|
|
821
824
|
if (isExternal) {
|
|
822
825
|
if (e.button === 0) {
|
|
823
826
|
this.#toggleSelection({ type: "point", id: hit });
|
|
@@ -859,12 +862,9 @@ export class SketchMode3D {
|
|
|
859
862
|
const idsRaw = Array.isArray(geo?.points) ? geo.points.slice() : [];
|
|
860
863
|
const ids = Array.from(new Set(idsRaw.map(x => parseInt(x))));
|
|
861
864
|
// Filter out external reference or fixed points (not draggable)
|
|
862
|
-
const f = this.#getSketchFeature();
|
|
863
|
-
const ext = (f?.persistentData?.externalRefs || []);
|
|
864
|
-
const isExternal = (pid) => ext.some(r => r.p0 === pid || r.p1 === pid);
|
|
865
865
|
const movable = ids.filter(pid => {
|
|
866
866
|
const p = this._solver?.getPointById?.(pid);
|
|
867
|
-
return p && !p.fixed && !
|
|
867
|
+
return p && !p.fixed && !this.#isExternalRefPointId(pid);
|
|
868
868
|
});
|
|
869
869
|
const uv = this.#pointerToPlaneUV(e);
|
|
870
870
|
this._pendingGeo = { ids: movable, x: e.clientX, y: e.clientY, startUV: uv, started: false, geometryId: ghit.id };
|
|
@@ -1558,7 +1558,14 @@ export class SketchMode3D {
|
|
|
1558
1558
|
if (!s) return null;
|
|
1559
1559
|
const pts = Array.isArray(s.points) ? s.points : (s.points = []);
|
|
1560
1560
|
const nextId = Math.max(0, ...pts.map((p) => +p.id || 0)) + 1;
|
|
1561
|
-
pts.push({
|
|
1561
|
+
pts.push({
|
|
1562
|
+
id: nextId,
|
|
1563
|
+
x: u,
|
|
1564
|
+
y: v,
|
|
1565
|
+
fixed: !!fixed,
|
|
1566
|
+
construction: false,
|
|
1567
|
+
externalReference: false,
|
|
1568
|
+
});
|
|
1562
1569
|
return nextId;
|
|
1563
1570
|
}
|
|
1564
1571
|
|
|
@@ -2135,11 +2142,15 @@ export class SketchMode3D {
|
|
|
2135
2142
|
next.constraints = Array.isArray(next.constraints) ? next.constraints : [];
|
|
2136
2143
|
|
|
2137
2144
|
if (!next.points.length) {
|
|
2138
|
-
next.points.push({ id: 0, x: 0, y: 0, fixed: true });
|
|
2145
|
+
next.points.push({ id: 0, x: 0, y: 0, fixed: true, construction: true, externalReference: false });
|
|
2139
2146
|
}
|
|
2140
2147
|
for (const pt of next.points) {
|
|
2141
|
-
if (pt
|
|
2142
|
-
|
|
2148
|
+
if (!pt) continue;
|
|
2149
|
+
pt.fixed = pt.fixed === true;
|
|
2150
|
+
if (typeof pt.construction !== "boolean") {
|
|
2151
|
+
pt.construction = Number(pt.id) === 0;
|
|
2152
|
+
}
|
|
2153
|
+
pt.externalReference = pt.externalReference === true;
|
|
2143
2154
|
}
|
|
2144
2155
|
const originId = Number(next.points[0]?.id);
|
|
2145
2156
|
const hasGround = Number.isFinite(originId) && next.constraints.some((c) =>
|
|
@@ -2190,7 +2201,7 @@ export class SketchMode3D {
|
|
|
2190
2201
|
if (!snapshot || !this._solver) return;
|
|
2191
2202
|
this._undoApplying = true;
|
|
2192
2203
|
try {
|
|
2193
|
-
this._solver.sketchObject = deepClone(snapshot.sketch || {});
|
|
2204
|
+
this._solver.sketchObject = this.#normalizeSketchInput(deepClone(snapshot.sketch || {}));
|
|
2194
2205
|
this._dimOffsets = snapshot.dimOffsets instanceof Map
|
|
2195
2206
|
? deepClone(snapshot.dimOffsets)
|
|
2196
2207
|
: new Map(snapshot.dimOffsets || []);
|
|
@@ -2199,6 +2210,7 @@ export class SketchMode3D {
|
|
|
2199
2210
|
feature.persistentData = feature.persistentData || {};
|
|
2200
2211
|
feature.persistentData.externalRefs = deepClone(snapshot.externalRefs || []);
|
|
2201
2212
|
}
|
|
2213
|
+
this.#refreshExternalRefPointIdsCache();
|
|
2202
2214
|
this._selection.clear();
|
|
2203
2215
|
this.#rebuildSketchGraphics();
|
|
2204
2216
|
this.#renderDimensions();
|
|
@@ -2275,6 +2287,49 @@ export class SketchMode3D {
|
|
|
2275
2287
|
return { u: d.dot(bx), v: d.dot(by) };
|
|
2276
2288
|
}
|
|
2277
2289
|
|
|
2290
|
+
#markLinkedEdgePoint(point, { forceConstruction = false } = {}) {
|
|
2291
|
+
if (!point) return;
|
|
2292
|
+
point.fixed = true;
|
|
2293
|
+
point.externalReference = true;
|
|
2294
|
+
if (forceConstruction || typeof point.construction !== "boolean") {
|
|
2295
|
+
point.construction = true;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
#refreshExternalRefPointIdsCache() {
|
|
2300
|
+
const ids = new Set();
|
|
2301
|
+
try {
|
|
2302
|
+
const refs = this.#getSketchFeature()?.persistentData?.externalRefs || [];
|
|
2303
|
+
for (const ref of refs) {
|
|
2304
|
+
const p0 = Number(ref?.p0);
|
|
2305
|
+
const p1 = Number(ref?.p1);
|
|
2306
|
+
if (Number.isFinite(p0)) ids.add(p0);
|
|
2307
|
+
if (Number.isFinite(p1)) ids.add(p1);
|
|
2308
|
+
}
|
|
2309
|
+
} catch { }
|
|
2310
|
+
this._externalRefPointIds = ids;
|
|
2311
|
+
|
|
2312
|
+
// Keep sketch-point metadata aligned with current external-ref table.
|
|
2313
|
+
const points = this._solver?.sketchObject?.points;
|
|
2314
|
+
if (Array.isArray(points)) {
|
|
2315
|
+
for (const point of points) {
|
|
2316
|
+
const pid = Number(point?.id);
|
|
2317
|
+
const isExternal = Number.isFinite(pid) && ids.has(pid);
|
|
2318
|
+
if (!point) continue;
|
|
2319
|
+
if (isExternal) this.#markLinkedEdgePoint(point, { forceConstruction: false });
|
|
2320
|
+
else point.externalReference = false;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
return ids;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
#isExternalRefPointId(pointId) {
|
|
2327
|
+
const pid = Number(pointId);
|
|
2328
|
+
if (!Number.isFinite(pid)) return false;
|
|
2329
|
+
if (!(this._externalRefPointIds instanceof Set)) this.#refreshExternalRefPointIdsCache();
|
|
2330
|
+
return (this._externalRefPointIds instanceof Set) && this._externalRefPointIds.has(pid);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2278
2333
|
// Ensure external refs exist for currently selected edges
|
|
2279
2334
|
#addExternalReferencesFromSelection() {
|
|
2280
2335
|
try {
|
|
@@ -2284,6 +2339,7 @@ export class SketchMode3D {
|
|
|
2284
2339
|
scene.traverse((obj) => { if (obj?.type === 'EDGE' && obj.selected) edges.push(obj); });
|
|
2285
2340
|
if (!edges.length) return;
|
|
2286
2341
|
for (const e of edges) this.#ensureExternalRefForEdge(e);
|
|
2342
|
+
this.#refreshExternalRefPointIdsCache();
|
|
2287
2343
|
this.#persistExternalRefs();
|
|
2288
2344
|
this._solver.solveSketch("full");
|
|
2289
2345
|
this.#rebuildSketchGraphics();
|
|
@@ -2315,8 +2371,8 @@ export class SketchMode3D {
|
|
|
2315
2371
|
// Note: calling nextPointId() twice without pushing in between would return the same value.
|
|
2316
2372
|
const id0 = nextPointId();
|
|
2317
2373
|
const id1 = id0 + 1;
|
|
2318
|
-
const p0 = { id: id0, x: uvA.u, y: uvA.v, fixed: true };
|
|
2319
|
-
const p1 = { id: id1, x: uvB.u, y: uvB.v, fixed: true };
|
|
2374
|
+
const p0 = { id: id0, x: uvA.u, y: uvA.v, fixed: true, construction: true, externalReference: true };
|
|
2375
|
+
const p1 = { id: id1, x: uvB.u, y: uvB.v, fixed: true, construction: true, externalReference: true };
|
|
2320
2376
|
s.points.push(p0, p1);
|
|
2321
2377
|
const pushGround = (pid) => {
|
|
2322
2378
|
const exists = s.constraints.some((c) => c.type === '⏚' && Array.isArray(c.points) && c.points[0] === pid);
|
|
@@ -2335,21 +2391,21 @@ export class SketchMode3D {
|
|
|
2335
2391
|
let pt1 = s.points.find((p) => p.id === ref.p1);
|
|
2336
2392
|
if (!pt0) {
|
|
2337
2393
|
const nid = nextPointId();
|
|
2338
|
-
pt0 = { id: nid, x: uvA.u, y: uvA.v, fixed: true };
|
|
2394
|
+
pt0 = { id: nid, x: uvA.u, y: uvA.v, fixed: true, construction: true, externalReference: true };
|
|
2339
2395
|
s.points.push(pt0);
|
|
2340
2396
|
ref.p0 = nid;
|
|
2341
2397
|
}
|
|
2342
2398
|
if (!pt1 || ref.p1 === ref.p0) {
|
|
2343
2399
|
const nid = Math.max(nextPointId(), pt0.id + 1);
|
|
2344
|
-
pt1 = { id: nid, x: uvB.u, y: uvB.v, fixed: true };
|
|
2400
|
+
pt1 = { id: nid, x: uvB.u, y: uvB.v, fixed: true, construction: true, externalReference: true };
|
|
2345
2401
|
s.points.push(pt1);
|
|
2346
2402
|
ref.p1 = nid;
|
|
2347
2403
|
}
|
|
2348
2404
|
// Ensure stored name metadata stays fresh
|
|
2349
2405
|
try { ref.edgeName = edge.name || ref.edgeName || null; } catch { }
|
|
2350
2406
|
try { ref.solidName = edge.parent?.name || ref.solidName || null; } catch { }
|
|
2351
|
-
if (pt0) { pt0.x = uvA.u; pt0.y = uvA.v; pt0
|
|
2352
|
-
if (pt1) { pt1.x = uvB.u; pt1.y = uvB.v; pt1
|
|
2407
|
+
if (pt0) { pt0.x = uvA.u; pt0.y = uvA.v; this.#markLinkedEdgePoint(pt0, { forceConstruction: false }); }
|
|
2408
|
+
if (pt1) { pt1.x = uvB.u; pt1.y = uvB.v; this.#markLinkedEdgePoint(pt1, { forceConstruction: false }); }
|
|
2353
2409
|
const ensureGround = (pid) => {
|
|
2354
2410
|
const exists = s.constraints.some((c) => c.type === '⏚' && Array.isArray(c.points) && c.points[0] === pid);
|
|
2355
2411
|
if (!exists) {
|
|
@@ -2404,7 +2460,7 @@ export class SketchMode3D {
|
|
|
2404
2460
|
// Repair legacy refs with missing/duplicate endpoint IDs
|
|
2405
2461
|
if (!pt0) {
|
|
2406
2462
|
const nid = Math.max(0, ...s.points.map((p) => +p.id || 0)) + 1;
|
|
2407
|
-
pt0 = { id: nid, x: uvA.u, y: uvA.v, fixed: true };
|
|
2463
|
+
pt0 = { id: nid, x: uvA.u, y: uvA.v, fixed: true, construction: true, externalReference: true };
|
|
2408
2464
|
s.points.push(pt0);
|
|
2409
2465
|
ref.p0 = nid;
|
|
2410
2466
|
changed = true;
|
|
@@ -2413,13 +2469,15 @@ export class SketchMode3D {
|
|
|
2413
2469
|
const nid = Math.max(0, ...s.points.map((p) => +p.id || 0)) + 1;
|
|
2414
2470
|
// Ensure pt1 ID is distinct from pt0
|
|
2415
2471
|
const id1 = (nid === pt0.id) ? nid + 1 : nid;
|
|
2416
|
-
pt1 = { id: id1, x: uvB.u, y: uvB.v, fixed: true };
|
|
2472
|
+
pt1 = { id: id1, x: uvB.u, y: uvB.v, fixed: true, construction: true, externalReference: true };
|
|
2417
2473
|
s.points.push(pt1);
|
|
2418
2474
|
ref.p1 = id1;
|
|
2419
2475
|
changed = true;
|
|
2420
2476
|
}
|
|
2421
|
-
if (pt0 && (pt0.x !== uvA.u || pt0.y !== uvA.v)) { pt0.x = uvA.u; pt0.y = uvA.v;
|
|
2422
|
-
if (pt1 && (pt1.x !== uvB.u || pt1.y !== uvB.v)) { pt1.x = uvB.u; pt1.y = uvB.v;
|
|
2477
|
+
if (pt0 && (pt0.x !== uvA.u || pt0.y !== uvA.v)) { pt0.x = uvA.u; pt0.y = uvA.v; changed = true; }
|
|
2478
|
+
if (pt1 && (pt1.x !== uvB.u || pt1.y !== uvB.v)) { pt1.x = uvB.u; pt1.y = uvB.v; changed = true; }
|
|
2479
|
+
if (pt0) this.#markLinkedEdgePoint(pt0, { forceConstruction: false });
|
|
2480
|
+
if (pt1) this.#markLinkedEdgePoint(pt1, { forceConstruction: false });
|
|
2423
2481
|
const ensureGround = (pid) => {
|
|
2424
2482
|
const exists = s.constraints.some((c) => c.type === '⏚' && Array.isArray(c.points) && c.points[0] === pid);
|
|
2425
2483
|
if (!exists) {
|
|
@@ -2433,6 +2491,7 @@ export class SketchMode3D {
|
|
|
2433
2491
|
} catch { }
|
|
2434
2492
|
}
|
|
2435
2493
|
if (changed || runSolve) {
|
|
2494
|
+
this.#refreshExternalRefPointIdsCache();
|
|
2436
2495
|
try { this._solver.solveSketch("full"); } catch { }
|
|
2437
2496
|
this.#rebuildSketchGraphics();
|
|
2438
2497
|
this.#refreshContextBar();
|
|
@@ -2490,8 +2549,13 @@ export class SketchMode3D {
|
|
|
2490
2549
|
const sObj = this._solver?.sketchObject;
|
|
2491
2550
|
if (sObj) {
|
|
2492
2551
|
sObj.constraints = sObj.constraints.filter((c) => !(c.type === '⏚' && Array.isArray(c.points) && (c.points[0] === r.p0 || c.points[0] === r.p1)));
|
|
2552
|
+
const p0 = sObj.points?.find((p) => p?.id === r.p0);
|
|
2553
|
+
const p1 = sObj.points?.find((p) => p?.id === r.p1);
|
|
2554
|
+
if (p0) p0.externalReference = false;
|
|
2555
|
+
if (p1) p1.externalReference = false;
|
|
2493
2556
|
}
|
|
2494
2557
|
} catch { }
|
|
2558
|
+
this.#refreshExternalRefPointIdsCache();
|
|
2495
2559
|
this.#persistExternalRefs();
|
|
2496
2560
|
this._solver?.solveSketch("full");
|
|
2497
2561
|
this.#rebuildSketchGraphics();
|
|
@@ -2655,21 +2719,31 @@ export class SketchMode3D {
|
|
|
2655
2719
|
if (this._secCurves)
|
|
2656
2720
|
this._secCurves.uiElement.innerHTML = (s.geometries || [])
|
|
2657
2721
|
.map((g) =>
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2722
|
+
{
|
|
2723
|
+
const constructionMarker = g?.construction ? " ◐" : "";
|
|
2724
|
+
return row(
|
|
2725
|
+
`${g.type}:${g.id}${constructionMarker} [${g.points?.join(",")}]`,
|
|
2726
|
+
`g:${g.id}`,
|
|
2727
|
+
`g:${g.id}`,
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2663
2730
|
)
|
|
2664
2731
|
.join("");
|
|
2665
2732
|
if (this._secPoints)
|
|
2666
2733
|
this._secPoints.uiElement.innerHTML = (s.points || [])
|
|
2667
2734
|
.map((p) =>
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2735
|
+
{
|
|
2736
|
+
const linked = this.#isExternalRefPointId(p?.id);
|
|
2737
|
+
const isOrigin = Number(p?.id) === 0;
|
|
2738
|
+
const linkMarker = linked ? " 🔗" : "";
|
|
2739
|
+
const fixedMarker = p?.fixed && !linked && !isOrigin ? " ⏚" : "";
|
|
2740
|
+
const constructionMarker = p?.construction ? " ◐" : "";
|
|
2741
|
+
return row(
|
|
2742
|
+
`P${p.id} (${p.x.toFixed(2)}, ${p.y.toFixed(2)})${linkMarker}${constructionMarker}${fixedMarker}`,
|
|
2743
|
+
`p:${p.id}`,
|
|
2744
|
+
`p:${p.id}`,
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2673
2747
|
)
|
|
2674
2748
|
.join("");
|
|
2675
2749
|
// Delegate clicks for selection
|
|
@@ -2817,6 +2891,18 @@ export class SketchMode3D {
|
|
|
2817
2891
|
tooltip: allFixed ? "Remove ground constraint" : "Add ground constraint",
|
|
2818
2892
|
onClick: () => this.#toggleGroundConstraints(selectedPointIds, allFixed),
|
|
2819
2893
|
});
|
|
2894
|
+
|
|
2895
|
+
const selPoints = selectedPointIds
|
|
2896
|
+
.map((pid) => s.points?.find((p) => Number(p?.id) === Number(pid)))
|
|
2897
|
+
.filter(Boolean);
|
|
2898
|
+
const allConstruction = selPoints.length > 0 && selPoints.every((p) => p.construction === true);
|
|
2899
|
+
appendButton({
|
|
2900
|
+
label: "◐",
|
|
2901
|
+
tooltip: allConstruction
|
|
2902
|
+
? "Include selected points in 3D result"
|
|
2903
|
+
: "Exclude selected points from 3D result (construction points)",
|
|
2904
|
+
onClick: () => this.#togglePointConstruction(selectedPointIds, allConstruction),
|
|
2905
|
+
});
|
|
2820
2906
|
}
|
|
2821
2907
|
|
|
2822
2908
|
// Constraint-specific actions
|
|
@@ -3259,6 +3345,32 @@ export class SketchMode3D {
|
|
|
3259
3345
|
this.#refreshContextBar();
|
|
3260
3346
|
}
|
|
3261
3347
|
|
|
3348
|
+
#togglePointConstruction(pointIds, removeConstruction) {
|
|
3349
|
+
const solver = this._solver;
|
|
3350
|
+
const sketch = solver?.sketchObject;
|
|
3351
|
+
if (!solver || !sketch || !Array.isArray(pointIds) || !pointIds.length) return;
|
|
3352
|
+
|
|
3353
|
+
const ids = pointIds
|
|
3354
|
+
.map((id) => parseInt(id))
|
|
3355
|
+
.filter((id) => Number.isFinite(id));
|
|
3356
|
+
if (!ids.length) return;
|
|
3357
|
+
|
|
3358
|
+
const nextConstruction = !removeConstruction;
|
|
3359
|
+
let changed = false;
|
|
3360
|
+
for (const pid of ids) {
|
|
3361
|
+
const point = sketch.points?.find((pt) => parseInt(pt?.id) === pid);
|
|
3362
|
+
if (!point) continue;
|
|
3363
|
+
if (point.construction === nextConstruction) continue;
|
|
3364
|
+
point.construction = nextConstruction;
|
|
3365
|
+
changed = true;
|
|
3366
|
+
}
|
|
3367
|
+
if (!changed) return;
|
|
3368
|
+
|
|
3369
|
+
try { solver.solveSketch("full"); } catch { }
|
|
3370
|
+
this.#rebuildSketchGraphics();
|
|
3371
|
+
this.#refreshContextBar();
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3262
3374
|
#reverseAngleConstraint(cid) {
|
|
3263
3375
|
const solver = this._solver;
|
|
3264
3376
|
const sketch = solver?.sketchObject;
|
|
@@ -4770,6 +4882,7 @@ export class SketchMode3D {
|
|
|
4770
4882
|
const s = this._solver.sketchObject;
|
|
4771
4883
|
const b = this._lock?.basis;
|
|
4772
4884
|
if (!b) return;
|
|
4885
|
+
this.#refreshExternalRefPointIdsCache();
|
|
4773
4886
|
const constrainedPoints = new Set();
|
|
4774
4887
|
try {
|
|
4775
4888
|
for (const c of s.constraints || []) {
|
|
@@ -4782,6 +4895,7 @@ export class SketchMode3D {
|
|
|
4782
4895
|
Y = b.y;
|
|
4783
4896
|
const geometryColor = this.#themeColor("geometryColor", 0xffff88);
|
|
4784
4897
|
const pointColor = this.#themeColor("pointColor", 0x9ec9ff);
|
|
4898
|
+
const constructionPointColor = this.#themeColor("constructionPointColor", 0xffa86a);
|
|
4785
4899
|
const curveThicknessPx = this.#themeNumber("curveThicknessPx", 1, 0.5, 48);
|
|
4786
4900
|
const useFatLines = this._useFatCurveLines === true || curveThicknessPx > 1;
|
|
4787
4901
|
const to3 = (u, v) =>
|
|
@@ -4933,8 +5047,12 @@ export class SketchMode3D {
|
|
|
4933
5047
|
const selected = Array.from(this._selection).some(
|
|
4934
5048
|
(it) => it.type === "point" && it.id === p.id,
|
|
4935
5049
|
);
|
|
5050
|
+
const isConstructionPoint = p?.construction === true;
|
|
4936
5051
|
const underConstrained = !selected && !p.fixed && !constrainedPoints.has(p.id);
|
|
4937
|
-
const
|
|
5052
|
+
const defaultPointColor = isConstructionPoint ? constructionPointColor : pointColor;
|
|
5053
|
+
const baseColor = (this._uniformPointColor || isConstructionPoint)
|
|
5054
|
+
? defaultPointColor
|
|
5055
|
+
: (underConstrained ? 0xffb347 : defaultPointColor);
|
|
4938
5056
|
const mat = new THREE.MeshBasicMaterial({
|
|
4939
5057
|
color: selected ? 0x6fe26f : baseColor,
|
|
4940
5058
|
depthTest: false,
|
|
@@ -4945,7 +5063,13 @@ export class SketchMode3D {
|
|
|
4945
5063
|
m.renderOrder = 10001;
|
|
4946
5064
|
|
|
4947
5065
|
m.position.copy(to3(p.x, p.y));
|
|
4948
|
-
m.userData = {
|
|
5066
|
+
m.userData = {
|
|
5067
|
+
kind: "point",
|
|
5068
|
+
id: p.id,
|
|
5069
|
+
underConstrained,
|
|
5070
|
+
isConstructionPoint,
|
|
5071
|
+
isExternalReference: this.#isExternalRefPointId(p.id),
|
|
5072
|
+
};
|
|
4949
5073
|
// Enlarge selected points 2x for better visibility
|
|
4950
5074
|
m.scale.setScalar(selected ? r * 2 : r);
|
|
4951
5075
|
grp.add(m);
|
|
@@ -21,6 +21,57 @@ function getConstraintBaseColor(inst) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function isLinkedEdgePoint(inst, pointId) {
|
|
25
|
+
const pid = Number(pointId);
|
|
26
|
+
if (!Number.isFinite(pid)) return false;
|
|
27
|
+
const cache = inst?._externalRefPointIds;
|
|
28
|
+
if (cache instanceof Set && cache.has(pid)) return true;
|
|
29
|
+
try {
|
|
30
|
+
const featureID = inst?.featureID;
|
|
31
|
+
const features = Array.isArray(inst?.viewer?.partHistory?.features)
|
|
32
|
+
? inst.viewer.partHistory.features
|
|
33
|
+
: [];
|
|
34
|
+
const feature = features.find((f) => f?.inputParams?.featureID === featureID) || null;
|
|
35
|
+
const refs = Array.isArray(feature?.persistentData?.externalRefs) ? feature.persistentData.externalRefs : [];
|
|
36
|
+
for (const ref of refs) {
|
|
37
|
+
if (Number(ref?.p0) === pid || Number(ref?.p1) === pid) return true;
|
|
38
|
+
}
|
|
39
|
+
} catch { }
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSketchOriginPoint(pointId) {
|
|
44
|
+
return Number(pointId) === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function forwardWheelToCanvas(inst, e) {
|
|
48
|
+
const canvas = inst?.viewer?.renderer?.domElement;
|
|
49
|
+
if (!canvas || !e) return;
|
|
50
|
+
let canceled = false;
|
|
51
|
+
try {
|
|
52
|
+
const forwarded = new WheelEvent(e.type, {
|
|
53
|
+
bubbles: true,
|
|
54
|
+
cancelable: true,
|
|
55
|
+
deltaX: e.deltaX,
|
|
56
|
+
deltaY: e.deltaY,
|
|
57
|
+
deltaZ: e.deltaZ,
|
|
58
|
+
deltaMode: e.deltaMode,
|
|
59
|
+
clientX: e.clientX,
|
|
60
|
+
clientY: e.clientY,
|
|
61
|
+
screenX: e.screenX,
|
|
62
|
+
screenY: e.screenY,
|
|
63
|
+
ctrlKey: e.ctrlKey,
|
|
64
|
+
shiftKey: e.shiftKey,
|
|
65
|
+
altKey: e.altKey,
|
|
66
|
+
metaKey: e.metaKey,
|
|
67
|
+
});
|
|
68
|
+
canceled = !canvas.dispatchEvent(forwarded);
|
|
69
|
+
} catch { /* ignore */ }
|
|
70
|
+
if (canceled) {
|
|
71
|
+
try { e.preventDefault(); } catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
24
75
|
function isRadialDimensionConstraint(c) {
|
|
25
76
|
return c?.type === '⟺'
|
|
26
77
|
&& Array.isArray(c.points)
|
|
@@ -178,6 +229,10 @@ export function renderDimensions(inst) {
|
|
|
178
229
|
|
|
179
230
|
const glyphConstraints = [];
|
|
180
231
|
for (const c of s.constraints || []) {
|
|
232
|
+
if (c?.type === '⏚' && Array.isArray(c?.points) && c.points.length > 0) {
|
|
233
|
+
const pointId = Number(c.points[0]);
|
|
234
|
+
if (isLinkedEdgePoint(inst, pointId) || isSketchOriginPoint(pointId)) continue;
|
|
235
|
+
}
|
|
181
236
|
const sel = Array.from(inst._selection || []).some(it => it.type === 'constraint' && it.id === c.id);
|
|
182
237
|
const hov = inst._hover && inst._hover.type === 'constraint' && inst._hover.id === c.id;
|
|
183
238
|
if (c.type === '⟺') {
|
|
@@ -363,6 +418,10 @@ function pointerToPlaneUV(inst, e) {
|
|
|
363
418
|
|
|
364
419
|
// Centralized event wiring for dimension labels (drag, click, hover, edit)
|
|
365
420
|
function attachDimLabelEvents(inst, el, c, world) {
|
|
421
|
+
el.addEventListener('wheel', (e) => {
|
|
422
|
+
forwardWheelToCanvas(inst, e);
|
|
423
|
+
}, { passive: false });
|
|
424
|
+
|
|
366
425
|
// Click: toggle constraint selection (dblclick handled separately)
|
|
367
426
|
el.addEventListener('click', (e) => {
|
|
368
427
|
if (e.detail > 1) return;
|
|
@@ -14,6 +14,34 @@ function themedConstraintColor(inst) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function forwardWheelToCanvas(inst, e) {
|
|
18
|
+
const canvas = inst?.viewer?.renderer?.domElement;
|
|
19
|
+
if (!canvas || !e) return;
|
|
20
|
+
let canceled = false;
|
|
21
|
+
try {
|
|
22
|
+
const forwarded = new WheelEvent(e.type, {
|
|
23
|
+
bubbles: true,
|
|
24
|
+
cancelable: true,
|
|
25
|
+
deltaX: e.deltaX,
|
|
26
|
+
deltaY: e.deltaY,
|
|
27
|
+
deltaZ: e.deltaZ,
|
|
28
|
+
deltaMode: e.deltaMode,
|
|
29
|
+
clientX: e.clientX,
|
|
30
|
+
clientY: e.clientY,
|
|
31
|
+
screenX: e.screenX,
|
|
32
|
+
screenY: e.screenY,
|
|
33
|
+
ctrlKey: e.ctrlKey,
|
|
34
|
+
shiftKey: e.shiftKey,
|
|
35
|
+
altKey: e.altKey,
|
|
36
|
+
metaKey: e.metaKey,
|
|
37
|
+
});
|
|
38
|
+
canceled = !canvas.dispatchEvent(forwarded);
|
|
39
|
+
} catch { /* ignore */ }
|
|
40
|
+
if (canceled) {
|
|
41
|
+
try { e.preventDefault(); } catch { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
// Grouped glyph renderer: draws small glyphs for non-dimension constraints,
|
|
18
46
|
// grouping those that act on the same set of points at a single location.
|
|
19
47
|
// Also records per-constraint centers for hit-testing.
|
|
@@ -65,6 +93,9 @@ export function drawConstraintGlyphs(inst, constraints) {
|
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
// Interactions: click to toggle selection; hover to reflect
|
|
96
|
+
el.addEventListener('wheel', (e) => {
|
|
97
|
+
forwardWheelToCanvas(inst, e);
|
|
98
|
+
}, { passive: false });
|
|
68
99
|
el.addEventListener('pointerdown', (e) => {
|
|
69
100
|
try { if (inst.viewer?.controls) inst.viewer.controls.enabled = false; } catch { }
|
|
70
101
|
try { el.setPointerCapture(e.pointerId); } catch { }
|
|
@@ -63,13 +63,15 @@ export function applyHoverAndSelectionColors(inst) {
|
|
|
63
63
|
const hov = inst._hover;
|
|
64
64
|
const themeGeometry = toHexColor(inst?._theme?.geometryColor, 0xffff88);
|
|
65
65
|
const themePoint = toHexColor(inst?._theme?.pointColor, 0x9ec9ff);
|
|
66
|
+
const themeConstructionPoint = toHexColor(inst?._theme?.constructionPointColor, 0xffa86a);
|
|
66
67
|
const useUnderConstrainedColor = inst?._uniformPointColor !== true;
|
|
67
68
|
const isSel = (kind, id) => Array.from(inst._selection).some(s => s.type === (kind === 'point' ? 'point' : 'geometry') && s.id === id);
|
|
68
69
|
const isHov = (kind, id) => hov && ((hov.type === 'point' && kind === 'point' && hov.id === id) || (hov.type === 'geometry' && kind === 'geometry' && hov.id === id));
|
|
69
70
|
for (const ch of inst._sketchGroup.children) {
|
|
70
71
|
const ud = ch.userData || {};
|
|
71
72
|
if (ud.kind === 'point') {
|
|
72
|
-
const
|
|
73
|
+
const pointBase = ud.isConstructionPoint ? themeConstructionPoint : themePoint;
|
|
74
|
+
const base = (useUnderConstrainedColor && ud.underConstrained && !ud.isConstructionPoint) ? 0xffb347 : pointBase;
|
|
73
75
|
const col = isSel('point', ud.id) ? 0x6fe26f : (isHov('point', ud.id) ? 0xffd54a : base);
|
|
74
76
|
try { ch.material.color.setHex(col); } catch {}
|
|
75
77
|
} else if (ud.kind === 'geometry') {
|