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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brep-io-kernel",
3
- "version": "1.0.98",
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
- const returnObject = updateInPlace ? this : Solid._fromManifold(outM, this._idToFaceName);
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 f = this.#getSketchFeature();
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 && !isExternal(pid);
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({ id: nextId, x: u, y: v, fixed: !!fixed });
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 && pt.fixed === true) continue;
2142
- if (pt) pt.fixed = false;
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.fixed = true; }
2352
- if (pt1) { pt1.x = uvB.u; pt1.y = uvB.v; pt1.fixed = true; }
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; pt0.fixed = true; changed = true; }
2422
- if (pt1 && (pt1.x !== uvB.u || pt1.y !== uvB.v)) { pt1.x = uvB.u; pt1.y = uvB.v; pt1.fixed = true; changed = true; }
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
- row(
2659
- `${g.type}:${g.id} [${g.points?.join(",")}]`,
2660
- `g:${g.id}`,
2661
- `g:${g.id}`,
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
- row(
2669
- `P${p.id} (${p.x.toFixed(2)}, ${p.y.toFixed(2)})${p.fixed ? " ⏚" : ""}`,
2670
- `p:${p.id}`,
2671
- `p:${p.id}`,
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 baseColor = (this._uniformPointColor ? pointColor : (underConstrained ? 0xffb347 : pointColor));
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 = { kind: "point", id: p.id, underConstrained };
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 base = (useUnderConstrainedColor && ud.underConstrained) ? 0xffb347 : themePoint;
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') {