void update boundary provenance
This commit is contained in:
parent
2efda6cf43
commit
f6bb8c1569
4 changed files with 598 additions and 7 deletions
|
|
@ -4,6 +4,28 @@
|
||||||
|
|
||||||
Track which portions of resulting solids come from which sketch extrude regions, including after union/subtract, while keeping storage compact and spline-ready.
|
Track which portions of resulting solids come from which sketch extrude regions, including after union/subtract, while keeping storage compact and spline-ready.
|
||||||
|
|
||||||
|
## Execution Status (2026-03-02)
|
||||||
|
|
||||||
|
Completed:
|
||||||
|
|
||||||
|
1. Plan authored and staged into phased implementation.
|
||||||
|
2. Debug visualization toggles added to preferences and wired to solids runtime:
|
||||||
|
3. Boundary loop rendering from GeometryStore.
|
||||||
|
4. Segment rendering from GeometryStore.
|
||||||
|
5. Segment/surface/region ID labels (overlay text).
|
||||||
|
6. Fixed world/local debug overlay transform bug:
|
||||||
|
7. GeometryStore points are world-space; line geometry parented under solids root must convert world -> root local because `space.WORLD` is rotated -90deg on X.
|
||||||
|
|
||||||
|
In progress:
|
||||||
|
|
||||||
|
1. Converting this plan into code-level milestone execution with strict acceptance checks.
|
||||||
|
|
||||||
|
Next up:
|
||||||
|
|
||||||
|
1. Add first `surface_patch_id` selection plumbing (read-only pass-through).
|
||||||
|
2. Thread Manifold relation metadata through kernel adapters.
|
||||||
|
3. Start mixed-face partition scaffold from seeded `surface_patches`.
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
1. Primary provenance is `boundary/region/surface-patch`, not raw triangle ownership.
|
1. Primary provenance is `boundary/region/surface-patch`, not raw triangle ownership.
|
||||||
|
|
@ -120,6 +142,7 @@ This enables deterministic attribution from boolean output back to input generat
|
||||||
|
|
||||||
1. Add `surface_patches` schema and runtime index container.
|
1. Add `surface_patches` schema and runtime index container.
|
||||||
2. Introduce generic segment schema (`kind + geom`) with current line/arc emitters.
|
2. Introduce generic segment schema (`kind + geom`) with current line/arc emitters.
|
||||||
|
3. Add compatibility normalizer for older docs (missing `surface_patches`).
|
||||||
|
|
||||||
### Phase 2: Kernel Metadata Plumbing
|
### Phase 2: Kernel Metadata Plumbing
|
||||||
|
|
||||||
|
|
@ -163,6 +186,39 @@ Regression guardrails:
|
||||||
1. No persisted triangle tables in doc snapshots.
|
1. No persisted triangle tables in doc snapshots.
|
||||||
2. No schema changes required to add spline segment kind later.
|
2. No schema changes required to add spline segment kind later.
|
||||||
3. Selection never reports mixed-source face entities.
|
3. Selection never reports mixed-source face entities.
|
||||||
|
4. Any debug/runtime geometry under solids root must explicitly convert GeometryStore world coordinates to root-local coordinates.
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
Phase 1:
|
||||||
|
|
||||||
|
1. Done: update `src/void/api/geometry_store.js` schema to include `surface_patches` and runtime topology patch map container.
|
||||||
|
2. Done: extend `buildGeometryStoreSnapshot()` in `src/void/api/solids.js` to emit seeded `surface_patches` per face-loop with source-region candidate fields.
|
||||||
|
3. In progress: keep current face-key canonical mapping intact while adding optional patch ID fields.
|
||||||
|
|
||||||
|
Phase 2:
|
||||||
|
|
||||||
|
1. Update `src/void/solid/kernel.js` mesh conversion to preserve manifold relation fields:
|
||||||
|
2. Input pass-through where provided (`runOriginalID`, `runIndex`, `faceID`, etc.).
|
||||||
|
3. Output pass-through into rebuild intermediate structures.
|
||||||
|
4. Add worker payload support for relation arrays when present.
|
||||||
|
|
||||||
|
Phase 3:
|
||||||
|
|
||||||
|
1. Implement mixed-source detection in `src/void/solid/rebuild.js`.
|
||||||
|
2. Add per-surface local partition pass and emit patch boundaries.
|
||||||
|
3. Assign one canonical `source_region_id` per patch.
|
||||||
|
|
||||||
|
Phase 4:
|
||||||
|
|
||||||
|
1. Extend selection resolver and solids hit mapping to prefer patch IDs over raw face IDs.
|
||||||
|
2. Add properties/tree hover mapping from extrude profile -> patch IDs.
|
||||||
|
3. Remove coarse fallback once parity checks pass.
|
||||||
|
|
||||||
|
Phase 5:
|
||||||
|
|
||||||
|
1. Add deterministic integration tests for union/subtract split-face provenance.
|
||||||
|
2. Remove temporary migration branches and finalize docs.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,12 @@ function createGeometryStoreApi(getApi) {
|
||||||
segments: [],
|
segments: [],
|
||||||
points: [],
|
points: [],
|
||||||
regions: [],
|
regions: [],
|
||||||
|
surface_patches: [],
|
||||||
topology: {
|
topology: {
|
||||||
surface_to_segments: {},
|
surface_to_segments: {},
|
||||||
segment_to_surfaces: {}
|
segment_to_surfaces: {},
|
||||||
|
patch_to_tris: {},
|
||||||
|
tri_to_patch: {}
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
feature_count: 0,
|
feature_count: 0,
|
||||||
|
|
@ -35,9 +38,16 @@ function createGeometryStoreApi(getApi) {
|
||||||
out.segments = Array.isArray(out.segments) ? out.segments : [];
|
out.segments = Array.isArray(out.segments) ? out.segments : [];
|
||||||
out.points = Array.isArray(out.points) ? out.points : [];
|
out.points = Array.isArray(out.points) ? out.points : [];
|
||||||
out.regions = Array.isArray(out.regions) ? out.regions : [];
|
out.regions = Array.isArray(out.regions) ? out.regions : [];
|
||||||
|
out.surface_patches = Array.isArray(out.surface_patches) ? out.surface_patches : [];
|
||||||
out.topology = out.topology && typeof out.topology === 'object'
|
out.topology = out.topology && typeof out.topology === 'object'
|
||||||
? out.topology
|
? out.topology
|
||||||
: base.topology;
|
: base.topology;
|
||||||
|
out.topology.patch_to_tris = out.topology.patch_to_tris && typeof out.topology.patch_to_tris === 'object'
|
||||||
|
? out.topology.patch_to_tris
|
||||||
|
: {};
|
||||||
|
out.topology.tri_to_patch = out.topology.tri_to_patch && typeof out.topology.tri_to_patch === 'object'
|
||||||
|
? out.topology.tri_to_patch
|
||||||
|
: {};
|
||||||
out.meta = out.meta && typeof out.meta === 'object'
|
out.meta = out.meta && typeof out.meta === 'object'
|
||||||
? out.meta
|
? out.meta
|
||||||
: base.meta;
|
: base.meta;
|
||||||
|
|
@ -84,6 +94,7 @@ function createGeometryStoreApi(getApi) {
|
||||||
state.segments = Array.isArray(snapshot.segments) ? snapshot.segments : [];
|
state.segments = Array.isArray(snapshot.segments) ? snapshot.segments : [];
|
||||||
state.points = Array.isArray(snapshot.points) ? snapshot.points : [];
|
state.points = Array.isArray(snapshot.points) ? snapshot.points : [];
|
||||||
state.regions = Array.isArray(snapshot.regions) ? snapshot.regions : [];
|
state.regions = Array.isArray(snapshot.regions) ? snapshot.regions : [];
|
||||||
|
state.surface_patches = Array.isArray(snapshot.surface_patches) ? snapshot.surface_patches : [];
|
||||||
state.topology = snapshot.topology && typeof snapshot.topology === 'object'
|
state.topology = snapshot.topology && typeof snapshot.topology === 'object'
|
||||||
? snapshot.topology
|
? snapshot.topology
|
||||||
: state.topology;
|
: state.topology;
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,26 @@ function edgeKey(a, b) {
|
||||||
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function colorFromId(id, sat = 70, light = 58) {
|
||||||
|
const raw = String(id || '');
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < raw.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
const hue = Math.abs(hash % 360);
|
||||||
|
const color = new THREE.Color();
|
||||||
|
color.setHSL(hue / 360, sat / 100, light / 100);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vec3FromRecord(rec) {
|
||||||
|
return new THREE.Vector3(
|
||||||
|
Number(rec?.x || 0),
|
||||||
|
Number(rec?.y || 0),
|
||||||
|
Number(rec?.z || 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function distancePointToSegmentSquared(p, a, b) {
|
function distancePointToSegmentSquared(p, a, b) {
|
||||||
const ab = new THREE.Vector3().subVectors(b, a);
|
const ab = new THREE.Vector3().subVectors(b, a);
|
||||||
const ap = new THREE.Vector3().subVectors(p, a);
|
const ap = new THREE.Vector3().subVectors(p, a);
|
||||||
|
|
@ -637,6 +657,14 @@ function edgeKey(a, b) {
|
||||||
edgeHoverLineWidth: 2.5,
|
edgeHoverLineWidth: 2.5,
|
||||||
edgeSelectedLineWidth: 3.25
|
edgeSelectedLineWidth: 3.25
|
||||||
},
|
},
|
||||||
|
_debugPrefs: {
|
||||||
|
showBoundaries: false,
|
||||||
|
showSegments: false,
|
||||||
|
showSegmentLabels: false,
|
||||||
|
showSurfaceLabels: false,
|
||||||
|
showRegionLabels: false,
|
||||||
|
showPatchLabels: false
|
||||||
|
},
|
||||||
_faceMats: null,
|
_faceMats: null,
|
||||||
_worker: null,
|
_worker: null,
|
||||||
_workerReady: false,
|
_workerReady: false,
|
||||||
|
|
@ -650,6 +678,8 @@ function edgeKey(a, b) {
|
||||||
_loopKeyByGeomBoundaryId: new Map(),
|
_loopKeyByGeomBoundaryId: new Map(),
|
||||||
_frozenChamferEdges: null,
|
_frozenChamferEdges: null,
|
||||||
_frozenEdgeOverlays: null,
|
_frozenEdgeOverlays: null,
|
||||||
|
_debugGroup: null,
|
||||||
|
_debugLabelIds: new Set(),
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await ensureKernel();
|
await ensureKernel();
|
||||||
|
|
@ -723,10 +753,353 @@ function edgeKey(a, b) {
|
||||||
this._root.name = 'void-solids-runtime';
|
this._root.name = 'void-solids-runtime';
|
||||||
this._frozenEdgeOverlays = new THREE.Group();
|
this._frozenEdgeOverlays = new THREE.Group();
|
||||||
this._frozenEdgeOverlays.name = 'void-solids-frozen-edge-overlays';
|
this._frozenEdgeOverlays.name = 'void-solids-frozen-edge-overlays';
|
||||||
|
this._debugGroup = new THREE.Group();
|
||||||
|
this._debugGroup.name = 'void-solids-debug-overlays';
|
||||||
this._root.add(this._frozenEdgeOverlays);
|
this._root.add(this._frozenEdgeOverlays);
|
||||||
|
this._root.add(this._debugGroup);
|
||||||
world?.add?.(this._root);
|
world?.add?.(this._root);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getSketchDerivedBoundarySegmentsForSolid(solid) {
|
||||||
|
const api = getApi();
|
||||||
|
if (!solid || String(solid?.source?.feature_type || '') !== 'extrude') return [];
|
||||||
|
const depth = Math.max(0.0001, Math.abs(Number(solid?.extrude?.depth ?? 0)));
|
||||||
|
if (!Number.isFinite(depth) || depth <= 0) return [];
|
||||||
|
const symmetric = solid?.extrude?.symmetric === true;
|
||||||
|
const direction = solid?.extrude?.direction === 'reverse' ? 'reverse' : 'normal';
|
||||||
|
const localZShift = symmetric ? (-depth / 2) : (direction === 'reverse' ? -depth : 0);
|
||||||
|
|
||||||
|
const keys = Array.isArray(solid?.source?.profile_keys) && solid.source.profile_keys.length
|
||||||
|
? solid.source.profile_keys
|
||||||
|
: (solid?.source?.profile?.region_id ? [solid.source.profile.region_id] : []);
|
||||||
|
if (!keys.length) return [];
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const ref = resolveProfileTargetRef({ region_id: String(key || '') });
|
||||||
|
if (!ref?.sketchId || !ref?.profileId) continue;
|
||||||
|
const sketch = api.features?.findById?.(ref.sketchId);
|
||||||
|
const basis = frameToBasis(sketch?.plane || null);
|
||||||
|
if (!basis) continue;
|
||||||
|
const loops = profileLoopsFromRuntime(api, { region_id: String(key || '') }) || [];
|
||||||
|
for (const loop of loops) {
|
||||||
|
if (!Array.isArray(loop) || loop.length < 3) continue;
|
||||||
|
const points = loop.map(p => ({ x: Number(p?.x || 0), y: Number(p?.y || 0) }));
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const a2 = points[i];
|
||||||
|
const b2 = points[(i + 1) % points.length];
|
||||||
|
const aStart = basis.origin.clone()
|
||||||
|
.addScaledVector(basis.xAxis, a2.x)
|
||||||
|
.addScaledVector(basis.yAxis, a2.y)
|
||||||
|
.addScaledVector(basis.normal, localZShift);
|
||||||
|
const bStart = basis.origin.clone()
|
||||||
|
.addScaledVector(basis.xAxis, b2.x)
|
||||||
|
.addScaledVector(basis.yAxis, b2.y)
|
||||||
|
.addScaledVector(basis.normal, localZShift);
|
||||||
|
const aEnd = aStart.clone().addScaledVector(basis.normal, depth);
|
||||||
|
const bEnd = bStart.clone().addScaledVector(basis.normal, depth);
|
||||||
|
segments.push({ a: aStart, b: bStart });
|
||||||
|
segments.push({ a: aEnd, b: bEnd });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSketchDerivedBoundarySegments() {
|
||||||
|
const out = [];
|
||||||
|
for (const solid of this.list() || []) {
|
||||||
|
const segs = this.getSketchDerivedBoundarySegmentsForSolid(solid);
|
||||||
|
if (segs?.length) out.push(...segs);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearDebugOverlays() {
|
||||||
|
if (this._debugGroup) {
|
||||||
|
while (this._debugGroup.children.length) {
|
||||||
|
const child = this._debugGroup.children[0];
|
||||||
|
child.geometry?.dispose?.();
|
||||||
|
child.material?.dispose?.();
|
||||||
|
this._debugGroup.remove(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const api = getApi();
|
||||||
|
for (const id of this._debugLabelIds) {
|
||||||
|
api.overlay?.remove?.(id);
|
||||||
|
}
|
||||||
|
this._debugLabelIds.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
syncDebugOverlays(snapshot = null) {
|
||||||
|
this.clearDebugOverlays();
|
||||||
|
const prefs = this._debugPrefs || {};
|
||||||
|
const enabled = !!(
|
||||||
|
prefs.showBoundaries
|
||||||
|
|| prefs.showSegments
|
||||||
|
|| prefs.showSegmentLabels
|
||||||
|
|| prefs.showSurfaceLabels
|
||||||
|
|| prefs.showRegionLabels
|
||||||
|
|| prefs.showPatchLabels
|
||||||
|
);
|
||||||
|
if (!enabled || !this._debugGroup) return;
|
||||||
|
|
||||||
|
const api = getApi();
|
||||||
|
const store = snapshot || api?.document?.current?.geometry_store || null;
|
||||||
|
const boundaries = Array.isArray(store?.boundaries) ? store.boundaries : [];
|
||||||
|
const segments = Array.isArray(store?.segments) ? store.segments : [];
|
||||||
|
const surfaces = Array.isArray(store?.surfaces) ? store.surfaces : [];
|
||||||
|
const regions = Array.isArray(store?.regions) ? store.regions : [];
|
||||||
|
const patches = Array.isArray(store?.surface_patches) ? store.surface_patches : [];
|
||||||
|
if (!boundaries.length && !segments.length && !surfaces.length && !regions.length) return;
|
||||||
|
|
||||||
|
const segById = new Map(segments.map(seg => [String(seg?.id || ''), seg]));
|
||||||
|
const boundaryById = new Map(boundaries.map(boundary => [String(boundary?.id || ''), boundary]));
|
||||||
|
const surfaceById = new Map(surfaces.map(surface => [String(surface?.id || ''), surface]));
|
||||||
|
const patchById = new Map(patches.map(patch => [String(patch?.id || ''), patch]));
|
||||||
|
this._root?.updateMatrixWorld?.(true);
|
||||||
|
|
||||||
|
// IMPORTANT: GeometryStore coordinates are scene/world-space.
|
||||||
|
// The solids runtime is attached under `space.WORLD`, which is rotated
|
||||||
|
// by -90deg on X in `space.js`. Any 3D debug geometry parented under
|
||||||
|
// this root MUST convert world -> root local first, or it will appear
|
||||||
|
// rotated/misaligned. Keep labels in world space (overlay expects world).
|
||||||
|
const toRootLocal = (worldVec3) => this._root?.worldToLocal?.(worldVec3.clone()) || worldVec3.clone();
|
||||||
|
|
||||||
|
const addLabel = (id, text, pos3d, color = '#ffd166') => {
|
||||||
|
if (!api.overlay || !pos3d || !text) return;
|
||||||
|
const labelId = `solid-debug:${id}`;
|
||||||
|
const opts = {
|
||||||
|
pos3d,
|
||||||
|
text,
|
||||||
|
color,
|
||||||
|
fontSize: 11,
|
||||||
|
className: 'overlay-text'
|
||||||
|
};
|
||||||
|
if (api.overlay.elements?.has?.(labelId)) {
|
||||||
|
api.overlay.update(labelId, opts);
|
||||||
|
} else {
|
||||||
|
api.overlay.add(labelId, 'text', opts);
|
||||||
|
}
|
||||||
|
this._debugLabelIds.add(labelId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hoveredFaceKey = String(this._hoveredFaceKey || '');
|
||||||
|
const hoveredSurfaceId = hoveredFaceKey
|
||||||
|
? (this._geomSurfaceIdByFaceKey.get(hoveredFaceKey) || `surface:${hoveredFaceKey}`)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const hoveredEdgeKey = String(this._hoveredEdgeKey || '');
|
||||||
|
let hoveredSegmentId = null;
|
||||||
|
let hoveredBoundaryId = null;
|
||||||
|
if (hoveredEdgeKey) {
|
||||||
|
hoveredSegmentId = this._geomSegmentIdByEdgeKey.get(hoveredEdgeKey) || null;
|
||||||
|
hoveredBoundaryId = this._geomBoundaryIdByLoopKey.get(hoveredEdgeKey) || null;
|
||||||
|
if (!hoveredSegmentId && !hoveredBoundaryId) {
|
||||||
|
const edge = this.getEdgeByKey(hoveredEdgeKey);
|
||||||
|
if (edge?.solidId && Number.isFinite(edge?.faceId) && Number.isFinite(edge?.index)) {
|
||||||
|
const basisKey = `faceedge:${edge.solidId}:${edge.faceId}:${edge.index}`;
|
||||||
|
hoveredSegmentId = this._geomSegmentIdByEdgeKey.get(basisKey) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hoveredBoundaryId && hoveredSegmentId) {
|
||||||
|
hoveredBoundaryId = String(segById.get(hoveredSegmentId)?.boundary_id || '') || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs.showBoundaries) {
|
||||||
|
const sketchDerived = this.getSketchDerivedBoundarySegments();
|
||||||
|
if (sketchDerived.length) {
|
||||||
|
const positions = [];
|
||||||
|
for (const seg of sketchDerived) {
|
||||||
|
// Sketch-derived loop points are generated from sketch plane
|
||||||
|
// frames in the same modeling frame as `space.WORLD` (root local).
|
||||||
|
// Do not apply an additional world->root conversion here or the
|
||||||
|
// debug boundaries will rotate off-axis by the WORLD transform.
|
||||||
|
const a = seg.a;
|
||||||
|
const b = seg.b;
|
||||||
|
positions.push(a.x, a.y, a.z, b.x, b.y, b.z);
|
||||||
|
}
|
||||||
|
if (positions.length) {
|
||||||
|
const geom = new THREE.BufferGeometry();
|
||||||
|
geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
|
const mat = new THREE.LineBasicMaterial({
|
||||||
|
color: 0x6ee7b7,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.95,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false
|
||||||
|
});
|
||||||
|
const lines = new THREE.LineSegments(geom, mat);
|
||||||
|
lines.renderOrder = 95;
|
||||||
|
this._debugGroup.add(lines);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const boundary of boundaries) {
|
||||||
|
const segmentIds = Array.isArray(boundary?.segment_ids) ? boundary.segment_ids : [];
|
||||||
|
if (!segmentIds.length) continue;
|
||||||
|
const positions = [];
|
||||||
|
for (const segmentId of segmentIds) {
|
||||||
|
const seg = segById.get(String(segmentId || ''));
|
||||||
|
if (!seg?.a || !seg?.b) continue;
|
||||||
|
const a = toRootLocal(vec3FromRecord(seg.a));
|
||||||
|
const b = toRootLocal(vec3FromRecord(seg.b));
|
||||||
|
positions.push(a.x, a.y, a.z, b.x, b.y, b.z);
|
||||||
|
}
|
||||||
|
if (!positions.length) continue;
|
||||||
|
const color = colorFromId(boundary?.id, 62, 56);
|
||||||
|
const geom = new THREE.BufferGeometry();
|
||||||
|
geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
|
const mat = new THREE.LineBasicMaterial({
|
||||||
|
color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.9,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false
|
||||||
|
});
|
||||||
|
const lines = new THREE.LineSegments(geom, mat);
|
||||||
|
lines.renderOrder = 95;
|
||||||
|
this._debugGroup.add(lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs.showSegments) {
|
||||||
|
for (const seg of segments) {
|
||||||
|
if (!seg?.a || !seg?.b) continue;
|
||||||
|
const a = toRootLocal(vec3FromRecord(seg.a));
|
||||||
|
const b = toRootLocal(vec3FromRecord(seg.b));
|
||||||
|
const geom = new THREE.BufferGeometry();
|
||||||
|
geom.setAttribute('position', new THREE.Float32BufferAttribute([
|
||||||
|
a.x, a.y, a.z,
|
||||||
|
b.x, b.y, b.z
|
||||||
|
], 3));
|
||||||
|
const mat = new THREE.LineBasicMaterial({
|
||||||
|
color: 0x66d9ef,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.95,
|
||||||
|
depthTest: false,
|
||||||
|
depthWrite: false
|
||||||
|
});
|
||||||
|
const line = new THREE.LineSegments(geom, mat);
|
||||||
|
line.renderOrder = 96;
|
||||||
|
this._debugGroup.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs.showSegmentLabels) {
|
||||||
|
const shown = [];
|
||||||
|
if (hoveredBoundaryId) {
|
||||||
|
const boundary = boundaryById.get(String(hoveredBoundaryId || ''));
|
||||||
|
const ids = Array.isArray(boundary?.segment_ids) ? boundary.segment_ids : [];
|
||||||
|
for (const sid of ids) {
|
||||||
|
const seg = segById.get(String(sid || ''));
|
||||||
|
if (seg) shown.push(seg);
|
||||||
|
}
|
||||||
|
} else if (hoveredSegmentId) {
|
||||||
|
const seg = segById.get(String(hoveredSegmentId || ''));
|
||||||
|
if (seg) shown.push(seg);
|
||||||
|
}
|
||||||
|
for (const seg of shown) {
|
||||||
|
const mid = seg?.mid || null;
|
||||||
|
if (!mid) continue;
|
||||||
|
const text = String(seg?.id || 'segment');
|
||||||
|
addLabel(`segment:${text}`, text, vec3FromRecord(mid), '#9ad9ff');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs.showSurfaceLabels) {
|
||||||
|
if (hoveredSurfaceId) {
|
||||||
|
const surface = surfaceById.get(String(hoveredSurfaceId || ''));
|
||||||
|
const center = surface?.center || null;
|
||||||
|
if (center) {
|
||||||
|
const text = String(surface?.id || 'surface');
|
||||||
|
addLabel(`surface:${text}`, text, vec3FromRecord(center), '#ffcf7a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs.showRegionLabels) {
|
||||||
|
for (const region of regions) {
|
||||||
|
if (hoveredSurfaceId && String(region?.surface_id || '') !== String(hoveredSurfaceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const boundaryIds = Array.isArray(region?.boundary_ids) ? region.boundary_ids : [];
|
||||||
|
const boundary = boundaryById.get(String(boundaryIds[0] || ''));
|
||||||
|
const segIds = Array.isArray(boundary?.segment_ids) ? boundary.segment_ids : [];
|
||||||
|
let anchor = null;
|
||||||
|
if (segIds.length) {
|
||||||
|
const sum = new THREE.Vector3();
|
||||||
|
let count = 0;
|
||||||
|
for (const sid of segIds) {
|
||||||
|
const seg = segById.get(String(sid || ''));
|
||||||
|
if (!seg?.mid) continue;
|
||||||
|
sum.add(vec3FromRecord(seg.mid));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count) {
|
||||||
|
anchor = sum.multiplyScalar(1 / count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anchor && region?.surface_id) {
|
||||||
|
const surface = surfaceById.get(String(region.surface_id));
|
||||||
|
if (surface?.center) anchor = vec3FromRecord(surface.center);
|
||||||
|
}
|
||||||
|
if (!anchor) continue;
|
||||||
|
const text = String(region?.id || 'region');
|
||||||
|
addLabel(`region:${text}`, text, anchor, '#ff9dc2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefs.showPatchLabels) {
|
||||||
|
const shown = [];
|
||||||
|
if (hoveredSurfaceId) {
|
||||||
|
for (const patch of patches) {
|
||||||
|
if (String(patch?.surface_id || '') === String(hoveredSurfaceId)) {
|
||||||
|
shown.push(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hoveredSurfaceId === null && hoveredEdgeKey) {
|
||||||
|
// edge hover only path fallback: show matching patch by boundary id
|
||||||
|
if (hoveredBoundaryId) {
|
||||||
|
for (const patch of patches) {
|
||||||
|
const bids = Array.isArray(patch?.boundary_ids) ? patch.boundary_ids : [];
|
||||||
|
if (bids.includes(hoveredBoundaryId)) shown.push(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const patch of shown) {
|
||||||
|
const regionId = String(patch?.source_region_id || '');
|
||||||
|
const boundaryIds = Array.isArray(patch?.boundary_ids) ? patch.boundary_ids : [];
|
||||||
|
const boundary = boundaryById.get(String(boundaryIds[0] || ''));
|
||||||
|
const segIds = Array.isArray(boundary?.segment_ids) ? boundary.segment_ids : [];
|
||||||
|
let anchor = null;
|
||||||
|
if (segIds.length) {
|
||||||
|
const sum = new THREE.Vector3();
|
||||||
|
let count = 0;
|
||||||
|
for (const sid of segIds) {
|
||||||
|
const seg = segById.get(String(sid || ''));
|
||||||
|
if (!seg?.mid) continue;
|
||||||
|
sum.add(vec3FromRecord(seg.mid));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count) anchor = sum.multiplyScalar(1 / count);
|
||||||
|
}
|
||||||
|
if (!anchor && patch?.surface_id) {
|
||||||
|
const surface = surfaceById.get(String(patch.surface_id));
|
||||||
|
if (surface?.center) anchor = vec3FromRecord(surface.center);
|
||||||
|
}
|
||||||
|
if (!anchor) continue;
|
||||||
|
const text = regionId
|
||||||
|
? `${String(patch?.id || 'patch')} -> ${regionId}`
|
||||||
|
: String(patch?.id || 'patch');
|
||||||
|
addLabel(`patch:${patch?.id || text}`, text, anchor, '#a6f57a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.overlay?.updateAll?.();
|
||||||
|
},
|
||||||
|
|
||||||
list() {
|
list() {
|
||||||
const api = getApi();
|
const api = getApi();
|
||||||
return Array.isArray(api.document.current?.generated?.solids)
|
return Array.isArray(api.document.current?.generated?.solids)
|
||||||
|
|
@ -735,6 +1108,7 @@ function edgeKey(a, b) {
|
||||||
},
|
},
|
||||||
|
|
||||||
buildGeometryStoreSnapshot() {
|
buildGeometryStoreSnapshot() {
|
||||||
|
this._root?.updateMatrixWorld?.(true);
|
||||||
this._geomSurfaceIdByFaceKey = new Map();
|
this._geomSurfaceIdByFaceKey = new Map();
|
||||||
this._geomSegmentIdByEdgeKey = new Map();
|
this._geomSegmentIdByEdgeKey = new Map();
|
||||||
this._geomBoundaryIdByLoopKey = new Map();
|
this._geomBoundaryIdByLoopKey = new Map();
|
||||||
|
|
@ -745,10 +1119,13 @@ function edgeKey(a, b) {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
const points = [];
|
const points = [];
|
||||||
const regions = [];
|
const regions = [];
|
||||||
|
const surface_patches = [];
|
||||||
const pointIdByKey = new Map();
|
const pointIdByKey = new Map();
|
||||||
const topology = {
|
const topology = {
|
||||||
surface_to_segments: {},
|
surface_to_segments: {},
|
||||||
segment_to_surfaces: {}
|
segment_to_surfaces: {},
|
||||||
|
patch_to_tris: {},
|
||||||
|
tri_to_patch: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPointId = (p, role = 'boundary-vertex') => {
|
const getPointId = (p, role = 'boundary-vertex') => {
|
||||||
|
|
@ -769,13 +1146,28 @@ function edgeKey(a, b) {
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [solidId, view] of this._meshViews.entries()) {
|
for (const [solidId, view] of this._meshViews.entries()) {
|
||||||
|
const solid = this.list().find(item => String(item?.id || '') === String(solidId)) || null;
|
||||||
|
const sourceProfileKeys = Array.isArray(solid?.source?.profile_keys)
|
||||||
|
? solid.source.profile_keys.map(key => String(key || '')).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const primarySourceRegion = sourceProfileKeys.length === 1 ? sourceProfileKeys[0] : null;
|
||||||
if (!view?.faceGroups?.size) continue;
|
if (!view?.faceGroups?.size) continue;
|
||||||
for (const [faceId, meta] of view.faceGroups.entries()) {
|
for (const [faceId, meta] of view.faceGroups.entries()) {
|
||||||
const faceKey = `${solidId}:${faceId}`;
|
const faceKey = `${solidId}:${faceId}`;
|
||||||
const surfaceId = `surface:${faceKey}`;
|
const surfaceId = `surface:${faceKey}`;
|
||||||
const loops = this.getFaceBoundaryLoops(faceKey) || [];
|
const loops = this.getFaceBoundaryLoops(faceKey) || [];
|
||||||
const normal = meta?.normal || {};
|
const mesh = view?.mesh || null;
|
||||||
const center = meta?.center || {};
|
const normalLocal = meta?.normal || new THREE.Vector3(0, 0, 1);
|
||||||
|
const centerLocal = meta?.center || new THREE.Vector3();
|
||||||
|
// GeometryStore should store scene/world-space coordinates.
|
||||||
|
// Face loops already do this via `getFaceBoundaryLoops()`.
|
||||||
|
// Keep surface center/normal in the same space for consistency.
|
||||||
|
const normal = normalLocal?.clone?.() && mesh?.matrixWorld
|
||||||
|
? normalLocal.clone().transformDirection(mesh.matrixWorld).normalize()
|
||||||
|
: normalLocal;
|
||||||
|
const center = centerLocal?.clone?.() && mesh?.matrixWorld
|
||||||
|
? centerLocal.clone().applyMatrix4(mesh.matrixWorld)
|
||||||
|
: centerLocal;
|
||||||
surfaces.push({
|
surfaces.push({
|
||||||
id: surfaceId,
|
id: surfaceId,
|
||||||
solid_id: solidId,
|
solid_id: solidId,
|
||||||
|
|
@ -868,6 +1260,24 @@ function edgeKey(a, b) {
|
||||||
loop_index: li
|
loop_index: li
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const patchId = `surface-patch:${faceKey}:${li}`;
|
||||||
|
const sourceRegionIds = sourceProfileKeys.slice();
|
||||||
|
surface_patches.push({
|
||||||
|
id: patchId,
|
||||||
|
surface_id: surfaceId,
|
||||||
|
boundary_ids: [boundaryId],
|
||||||
|
source_region_id: primarySourceRegion,
|
||||||
|
source_region_ids: sourceRegionIds,
|
||||||
|
solid_id: solidId,
|
||||||
|
face_id: faceId,
|
||||||
|
status: 'seed',
|
||||||
|
source: {
|
||||||
|
type: 'solid-face-loop',
|
||||||
|
face_key: faceKey,
|
||||||
|
loop_index: li,
|
||||||
|
feature_id: solid?.source?.feature_id || null
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
topology.surface_to_segments[surfaceId] = surfaceSegmentIds;
|
topology.surface_to_segments[surfaceId] = surfaceSegmentIds;
|
||||||
}
|
}
|
||||||
|
|
@ -878,6 +1288,7 @@ function edgeKey(a, b) {
|
||||||
segments,
|
segments,
|
||||||
points,
|
points,
|
||||||
regions,
|
regions,
|
||||||
|
surface_patches,
|
||||||
topology,
|
topology,
|
||||||
meta: {
|
meta: {
|
||||||
feature_count: Number(getApi()?.features?.list?.()?.length || 0)
|
feature_count: Number(getApi()?.features?.list?.()?.length || 0)
|
||||||
|
|
@ -1080,6 +1491,9 @@ function edgeKey(a, b) {
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const snapshot = this.buildGeometryStoreSnapshot();
|
const snapshot = this.buildGeometryStoreSnapshot();
|
||||||
api.geometryStore?.applySolidSnapshot?.(doc, snapshot);
|
api.geometryStore?.applySolidSnapshot?.(doc, snapshot);
|
||||||
|
this.syncDebugOverlays(snapshot);
|
||||||
|
} else {
|
||||||
|
this.syncDebugOverlays(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1984,6 +2398,9 @@ function edgeKey(a, b) {
|
||||||
if (next === this._hoveredFaceKey) return;
|
if (next === this._hoveredFaceKey) return;
|
||||||
this._hoveredFaceKey = next;
|
this._hoveredFaceKey = next;
|
||||||
this.syncFaceOverlays();
|
this.syncFaceOverlays();
|
||||||
|
if (this._debugPrefs?.showSurfaceLabels || this._debugPrefs?.showRegionLabels) {
|
||||||
|
this.syncDebugOverlays();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setSelectedFaces(keys = []) {
|
setSelectedFaces(keys = []) {
|
||||||
|
|
@ -2015,6 +2432,9 @@ function edgeKey(a, b) {
|
||||||
if (next === this._hoveredEdgeKey) return;
|
if (next === this._hoveredEdgeKey) return;
|
||||||
this._hoveredEdgeKey = next;
|
this._hoveredEdgeKey = next;
|
||||||
this.syncEdgeOverlays();
|
this.syncEdgeOverlays();
|
||||||
|
if (this._debugPrefs?.showSegmentLabels) {
|
||||||
|
this.syncDebugOverlays();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setSelectedEdges(keys = []) {
|
setSelectedEdges(keys = []) {
|
||||||
|
|
@ -2057,6 +2477,25 @@ function edgeKey(a, b) {
|
||||||
return this.getRenderPreferences();
|
return this.getRenderPreferences();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDebugPreferences() {
|
||||||
|
return { ...(this._debugPrefs || {}) };
|
||||||
|
},
|
||||||
|
|
||||||
|
setDebugPreferences(next = {}) {
|
||||||
|
const curr = this._debugPrefs || {};
|
||||||
|
const merged = {
|
||||||
|
showBoundaries: next.showBoundaries !== undefined ? next.showBoundaries === true : !!curr.showBoundaries,
|
||||||
|
showSegments: next.showSegments !== undefined ? next.showSegments === true : !!curr.showSegments,
|
||||||
|
showSegmentLabels: next.showSegmentLabels !== undefined ? next.showSegmentLabels === true : !!curr.showSegmentLabels,
|
||||||
|
showSurfaceLabels: next.showSurfaceLabels !== undefined ? next.showSurfaceLabels === true : !!curr.showSurfaceLabels,
|
||||||
|
showRegionLabels: next.showRegionLabels !== undefined ? next.showRegionLabels === true : !!curr.showRegionLabels,
|
||||||
|
showPatchLabels: next.showPatchLabels !== undefined ? next.showPatchLabels === true : !!curr.showPatchLabels
|
||||||
|
};
|
||||||
|
this._debugPrefs = merged;
|
||||||
|
this.syncDebugOverlays();
|
||||||
|
return this.getDebugPreferences();
|
||||||
|
},
|
||||||
|
|
||||||
getSketchTargetForFaceKey(key) {
|
getSketchTargetForFaceKey(key) {
|
||||||
const face = this.getFaceByKey(key);
|
const face = this.getFaceByKey(key);
|
||||||
if (!face) return null;
|
if (!face) return null;
|
||||||
|
|
|
||||||
|
|
@ -1082,12 +1082,19 @@ const toolbar = {
|
||||||
edgeSelectedLineWidth: 3.25,
|
edgeSelectedLineWidth: 3.25,
|
||||||
sketchArcSegmentLength: 2.5,
|
sketchArcSegmentLength: 2.5,
|
||||||
fitPaddingPerspective: 0.5,
|
fitPaddingPerspective: 0.5,
|
||||||
fitPaddingOrthographic: 0.9
|
fitPaddingOrthographic: 0.9,
|
||||||
|
debugShowBoundaries: false,
|
||||||
|
debugShowSegments: false,
|
||||||
|
debugShowSegmentLabels: false,
|
||||||
|
debugShowSurfaceLabels: false,
|
||||||
|
debugShowRegionLabels: false,
|
||||||
|
debugShowPatchLabels: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
normalizePreferences(raw = {}) {
|
normalizePreferences(raw = {}) {
|
||||||
const d = this.getDefaultPreferences();
|
const d = this.getDefaultPreferences();
|
||||||
|
const toBool = value => value === true || value === 'true' || value === 1 || value === '1';
|
||||||
return {
|
return {
|
||||||
edgeLoopPromotionSegments: Math.max(3, Math.round(Number(raw.edgeLoopPromotionSegments ?? d.edgeLoopPromotionSegments) || d.edgeLoopPromotionSegments)),
|
edgeLoopPromotionSegments: Math.max(3, Math.round(Number(raw.edgeLoopPromotionSegments ?? d.edgeLoopPromotionSegments) || d.edgeLoopPromotionSegments)),
|
||||||
edgeHoverLineWidth: Math.max(0.5, Number(raw.edgeHoverLineWidth ?? d.edgeHoverLineWidth) || d.edgeHoverLineWidth),
|
edgeHoverLineWidth: Math.max(0.5, Number(raw.edgeHoverLineWidth ?? d.edgeHoverLineWidth) || d.edgeHoverLineWidth),
|
||||||
|
|
@ -1097,7 +1104,13 @@ const toolbar = {
|
||||||
Number(raw.sketchArcSegmentLength ?? raw.sketchArcSegments ?? d.sketchArcSegmentLength) || d.sketchArcSegmentLength
|
Number(raw.sketchArcSegmentLength ?? raw.sketchArcSegments ?? d.sketchArcSegmentLength) || d.sketchArcSegmentLength
|
||||||
),
|
),
|
||||||
fitPaddingPerspective: Math.max(0.01, Number(raw.fitPaddingPerspective ?? d.fitPaddingPerspective) || d.fitPaddingPerspective),
|
fitPaddingPerspective: Math.max(0.01, Number(raw.fitPaddingPerspective ?? d.fitPaddingPerspective) || d.fitPaddingPerspective),
|
||||||
fitPaddingOrthographic: Math.max(0.01, Number(raw.fitPaddingOrthographic ?? d.fitPaddingOrthographic) || d.fitPaddingOrthographic)
|
fitPaddingOrthographic: Math.max(0.01, Number(raw.fitPaddingOrthographic ?? d.fitPaddingOrthographic) || d.fitPaddingOrthographic),
|
||||||
|
debugShowBoundaries: toBool(raw.debugShowBoundaries ?? d.debugShowBoundaries),
|
||||||
|
debugShowSegments: toBool(raw.debugShowSegments ?? d.debugShowSegments),
|
||||||
|
debugShowSegmentLabels: toBool(raw.debugShowSegmentLabels ?? d.debugShowSegmentLabels),
|
||||||
|
debugShowSurfaceLabels: toBool(raw.debugShowSurfaceLabels ?? d.debugShowSurfaceLabels),
|
||||||
|
debugShowRegionLabels: toBool(raw.debugShowRegionLabels ?? d.debugShowRegionLabels),
|
||||||
|
debugShowPatchLabels: toBool(raw.debugShowPatchLabels ?? d.debugShowPatchLabels)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1137,6 +1150,14 @@ const toolbar = {
|
||||||
edgeHoverLineWidth: prefs.edgeHoverLineWidth,
|
edgeHoverLineWidth: prefs.edgeHoverLineWidth,
|
||||||
edgeSelectedLineWidth: prefs.edgeSelectedLineWidth
|
edgeSelectedLineWidth: prefs.edgeSelectedLineWidth
|
||||||
});
|
});
|
||||||
|
api.solids?.setDebugPreferences?.({
|
||||||
|
showBoundaries: prefs.debugShowBoundaries,
|
||||||
|
showSegments: prefs.debugShowSegments,
|
||||||
|
showSegmentLabels: prefs.debugShowSegmentLabels,
|
||||||
|
showSurfaceLabels: prefs.debugShowSurfaceLabels,
|
||||||
|
showRegionLabels: prefs.debugShowRegionLabels,
|
||||||
|
showPatchLabels: prefs.debugShowPatchLabels
|
||||||
|
});
|
||||||
api.sketchRuntime?.setRenderPreferences?.({
|
api.sketchRuntime?.setRenderPreferences?.({
|
||||||
arcSegmentLength: prefs.sketchArcSegmentLength
|
arcSegmentLength: prefs.sketchArcSegmentLength
|
||||||
});
|
});
|
||||||
|
|
@ -1161,6 +1182,12 @@ const toolbar = {
|
||||||
if (inputs.sketchArcSegmentLength) inputs.sketchArcSegmentLength.value = String(prefs.sketchArcSegmentLength);
|
if (inputs.sketchArcSegmentLength) inputs.sketchArcSegmentLength.value = String(prefs.sketchArcSegmentLength);
|
||||||
if (inputs.fitPaddingPerspective) inputs.fitPaddingPerspective.value = String(prefs.fitPaddingPerspective);
|
if (inputs.fitPaddingPerspective) inputs.fitPaddingPerspective.value = String(prefs.fitPaddingPerspective);
|
||||||
if (inputs.fitPaddingOrthographic) inputs.fitPaddingOrthographic.value = String(prefs.fitPaddingOrthographic);
|
if (inputs.fitPaddingOrthographic) inputs.fitPaddingOrthographic.value = String(prefs.fitPaddingOrthographic);
|
||||||
|
if (inputs.debugShowBoundaries) inputs.debugShowBoundaries.checked = !!prefs.debugShowBoundaries;
|
||||||
|
if (inputs.debugShowSegments) inputs.debugShowSegments.checked = !!prefs.debugShowSegments;
|
||||||
|
if (inputs.debugShowSegmentLabels) inputs.debugShowSegmentLabels.checked = !!prefs.debugShowSegmentLabels;
|
||||||
|
if (inputs.debugShowSurfaceLabels) inputs.debugShowSurfaceLabels.checked = !!prefs.debugShowSurfaceLabels;
|
||||||
|
if (inputs.debugShowRegionLabels) inputs.debugShowRegionLabels.checked = !!prefs.debugShowRegionLabels;
|
||||||
|
if (inputs.debugShowPatchLabels) inputs.debugShowPatchLabels.checked = !!prefs.debugShowPatchLabels;
|
||||||
},
|
},
|
||||||
|
|
||||||
buildPreferencesDialog() {
|
buildPreferencesDialog() {
|
||||||
|
|
@ -1201,6 +1228,28 @@ const toolbar = {
|
||||||
this.preferencesInputs = this.preferencesInputs || {};
|
this.preferencesInputs = this.preferencesInputs || {};
|
||||||
this.preferencesInputs[key] = input;
|
this.preferencesInputs[key] = input;
|
||||||
};
|
};
|
||||||
|
const makeCheckboxRow = (label, key, help = '') => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'doc-dialog-row prefs-row';
|
||||||
|
const name = document.createElement('div');
|
||||||
|
name.className = 'doc-dialog-name';
|
||||||
|
name.textContent = label;
|
||||||
|
if (help) {
|
||||||
|
name.title = help;
|
||||||
|
row.title = help;
|
||||||
|
}
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.className = 'prefs-input';
|
||||||
|
if (help) {
|
||||||
|
input.title = help;
|
||||||
|
}
|
||||||
|
row.appendChild(name);
|
||||||
|
row.appendChild(input);
|
||||||
|
list.appendChild(row);
|
||||||
|
this.preferencesInputs = this.preferencesInputs || {};
|
||||||
|
this.preferencesInputs[key] = input;
|
||||||
|
};
|
||||||
|
|
||||||
makeNumberRow(
|
makeNumberRow(
|
||||||
'Edge Loop Promotion Segments',
|
'Edge Loop Promotion Segments',
|
||||||
|
|
@ -1238,6 +1287,36 @@ const toolbar = {
|
||||||
'0.01',
|
'0.01',
|
||||||
'Extra margin used by Fit view in orthographic mode. Tune this separately from perspective for CAD-like framing.'
|
'Extra margin used by Fit view in orthographic mode. Tune this separately from perspective for CAD-like framing.'
|
||||||
);
|
);
|
||||||
|
makeCheckboxRow(
|
||||||
|
'Debug: Show Boundaries',
|
||||||
|
'debugShowBoundaries',
|
||||||
|
'Render boundary loops from GeometryStore in 3D to verify boundary extraction and provenance partitioning.'
|
||||||
|
);
|
||||||
|
makeCheckboxRow(
|
||||||
|
'Debug: Show Segments',
|
||||||
|
'debugShowSegments',
|
||||||
|
'Render every boundary segment from GeometryStore in cyan for dense topology inspection.'
|
||||||
|
);
|
||||||
|
makeCheckboxRow(
|
||||||
|
'Debug: Segment Labels',
|
||||||
|
'debugShowSegmentLabels',
|
||||||
|
'Draw 2D overlay labels for segment IDs at segment midpoints.'
|
||||||
|
);
|
||||||
|
makeCheckboxRow(
|
||||||
|
'Debug: Surface Labels',
|
||||||
|
'debugShowSurfaceLabels',
|
||||||
|
'Draw 2D overlay labels for canonical surface IDs at surface centers.'
|
||||||
|
);
|
||||||
|
makeCheckboxRow(
|
||||||
|
'Debug: Region Labels',
|
||||||
|
'debugShowRegionLabels',
|
||||||
|
'Draw 2D overlay labels for canonical region IDs using boundary centroid anchors.'
|
||||||
|
);
|
||||||
|
makeCheckboxRow(
|
||||||
|
'Debug: Patch Labels',
|
||||||
|
'debugShowPatchLabels',
|
||||||
|
'Draw hover-scoped labels for seeded surface patch IDs and mapped source region IDs.'
|
||||||
|
);
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'doc-dialog-actions';
|
actions.className = 'doc-dialog-actions';
|
||||||
const defaultsBtn = this.addButton(actions, 'Defaults', () => {
|
const defaultsBtn = this.addButton(actions, 'Defaults', () => {
|
||||||
|
|
@ -1251,7 +1330,13 @@ const toolbar = {
|
||||||
edgeSelectedLineWidth: Number(this.preferencesInputs?.edgeSelectedLineWidth?.value),
|
edgeSelectedLineWidth: Number(this.preferencesInputs?.edgeSelectedLineWidth?.value),
|
||||||
sketchArcSegmentLength: Number(this.preferencesInputs?.sketchArcSegmentLength?.value),
|
sketchArcSegmentLength: Number(this.preferencesInputs?.sketchArcSegmentLength?.value),
|
||||||
fitPaddingPerspective: Number(this.preferencesInputs?.fitPaddingPerspective?.value),
|
fitPaddingPerspective: Number(this.preferencesInputs?.fitPaddingPerspective?.value),
|
||||||
fitPaddingOrthographic: Number(this.preferencesInputs?.fitPaddingOrthographic?.value)
|
fitPaddingOrthographic: Number(this.preferencesInputs?.fitPaddingOrthographic?.value),
|
||||||
|
debugShowBoundaries: !!this.preferencesInputs?.debugShowBoundaries?.checked,
|
||||||
|
debugShowSegments: !!this.preferencesInputs?.debugShowSegments?.checked,
|
||||||
|
debugShowSegmentLabels: !!this.preferencesInputs?.debugShowSegmentLabels?.checked,
|
||||||
|
debugShowSurfaceLabels: !!this.preferencesInputs?.debugShowSurfaceLabels?.checked,
|
||||||
|
debugShowRegionLabels: !!this.preferencesInputs?.debugShowRegionLabels?.checked,
|
||||||
|
debugShowPatchLabels: !!this.preferencesInputs?.debugShowPatchLabels?.checked
|
||||||
}, { persist: true, updateFields: true });
|
}, { persist: true, updateFields: true });
|
||||||
});
|
});
|
||||||
applyBtn.classList.add('compact');
|
applyBtn.classList.add('compact');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue