void update boundary provenance

This commit is contained in:
Stewart Allen 2026-03-02 14:24:30 -05:00
commit f6bb8c1569
4 changed files with 598 additions and 7 deletions

View file

@ -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.
## 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
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.
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
@ -163,6 +186,39 @@ Regression guardrails:
1. No persisted triangle tables in doc snapshots.
2. No schema changes required to add spline segment kind later.
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

View file

@ -13,9 +13,12 @@ function createGeometryStoreApi(getApi) {
segments: [],
points: [],
regions: [],
surface_patches: [],
topology: {
surface_to_segments: {},
segment_to_surfaces: {}
segment_to_surfaces: {},
patch_to_tris: {},
tri_to_patch: {}
},
meta: {
feature_count: 0,
@ -35,9 +38,16 @@ function createGeometryStoreApi(getApi) {
out.segments = Array.isArray(out.segments) ? out.segments : [];
out.points = Array.isArray(out.points) ? out.points : [];
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
: 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
: base.meta;
@ -84,6 +94,7 @@ function createGeometryStoreApi(getApi) {
state.segments = Array.isArray(snapshot.segments) ? snapshot.segments : [];
state.points = Array.isArray(snapshot.points) ? snapshot.points : [];
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'
? snapshot.topology
: state.topology;

View file

@ -156,6 +156,26 @@ function edgeKey(a, b) {
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) {
const ab = new THREE.Vector3().subVectors(b, a);
const ap = new THREE.Vector3().subVectors(p, a);
@ -637,6 +657,14 @@ function edgeKey(a, b) {
edgeHoverLineWidth: 2.5,
edgeSelectedLineWidth: 3.25
},
_debugPrefs: {
showBoundaries: false,
showSegments: false,
showSegmentLabels: false,
showSurfaceLabels: false,
showRegionLabels: false,
showPatchLabels: false
},
_faceMats: null,
_worker: null,
_workerReady: false,
@ -650,6 +678,8 @@ function edgeKey(a, b) {
_loopKeyByGeomBoundaryId: new Map(),
_frozenChamferEdges: null,
_frozenEdgeOverlays: null,
_debugGroup: null,
_debugLabelIds: new Set(),
async init() {
await ensureKernel();
@ -723,10 +753,353 @@ function edgeKey(a, b) {
this._root.name = 'void-solids-runtime';
this._frozenEdgeOverlays = new THREE.Group();
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._debugGroup);
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() {
const api = getApi();
return Array.isArray(api.document.current?.generated?.solids)
@ -735,6 +1108,7 @@ function edgeKey(a, b) {
},
buildGeometryStoreSnapshot() {
this._root?.updateMatrixWorld?.(true);
this._geomSurfaceIdByFaceKey = new Map();
this._geomSegmentIdByEdgeKey = new Map();
this._geomBoundaryIdByLoopKey = new Map();
@ -745,10 +1119,13 @@ function edgeKey(a, b) {
const segments = [];
const points = [];
const regions = [];
const surface_patches = [];
const pointIdByKey = new Map();
const topology = {
surface_to_segments: {},
segment_to_surfaces: {}
segment_to_surfaces: {},
patch_to_tris: {},
tri_to_patch: {}
};
const getPointId = (p, role = 'boundary-vertex') => {
@ -769,13 +1146,28 @@ function edgeKey(a, b) {
};
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;
for (const [faceId, meta] of view.faceGroups.entries()) {
const faceKey = `${solidId}:${faceId}`;
const surfaceId = `surface:${faceKey}`;
const loops = this.getFaceBoundaryLoops(faceKey) || [];
const normal = meta?.normal || {};
const center = meta?.center || {};
const mesh = view?.mesh || null;
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({
id: surfaceId,
solid_id: solidId,
@ -868,6 +1260,24 @@ function edgeKey(a, b) {
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;
}
@ -878,6 +1288,7 @@ function edgeKey(a, b) {
segments,
points,
regions,
surface_patches,
topology,
meta: {
feature_count: Number(getApi()?.features?.list?.()?.length || 0)
@ -1080,6 +1491,9 @@ function edgeKey(a, b) {
if (doc) {
const snapshot = this.buildGeometryStoreSnapshot();
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;
this._hoveredFaceKey = next;
this.syncFaceOverlays();
if (this._debugPrefs?.showSurfaceLabels || this._debugPrefs?.showRegionLabels) {
this.syncDebugOverlays();
}
},
setSelectedFaces(keys = []) {
@ -2015,6 +2432,9 @@ function edgeKey(a, b) {
if (next === this._hoveredEdgeKey) return;
this._hoveredEdgeKey = next;
this.syncEdgeOverlays();
if (this._debugPrefs?.showSegmentLabels) {
this.syncDebugOverlays();
}
},
setSelectedEdges(keys = []) {
@ -2057,6 +2477,25 @@ function edgeKey(a, b) {
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) {
const face = this.getFaceByKey(key);
if (!face) return null;

View file

@ -1082,12 +1082,19 @@ const toolbar = {
edgeSelectedLineWidth: 3.25,
sketchArcSegmentLength: 2.5,
fitPaddingPerspective: 0.5,
fitPaddingOrthographic: 0.9
fitPaddingOrthographic: 0.9,
debugShowBoundaries: false,
debugShowSegments: false,
debugShowSegmentLabels: false,
debugShowSurfaceLabels: false,
debugShowRegionLabels: false,
debugShowPatchLabels: false
};
},
normalizePreferences(raw = {}) {
const d = this.getDefaultPreferences();
const toBool = value => value === true || value === 'true' || value === 1 || value === '1';
return {
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),
@ -1097,7 +1104,13 @@ const toolbar = {
Number(raw.sketchArcSegmentLength ?? raw.sketchArcSegments ?? d.sketchArcSegmentLength) || d.sketchArcSegmentLength
),
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,
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?.({
arcSegmentLength: prefs.sketchArcSegmentLength
});
@ -1161,6 +1182,12 @@ const toolbar = {
if (inputs.sketchArcSegmentLength) inputs.sketchArcSegmentLength.value = String(prefs.sketchArcSegmentLength);
if (inputs.fitPaddingPerspective) inputs.fitPaddingPerspective.value = String(prefs.fitPaddingPerspective);
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() {
@ -1201,6 +1228,28 @@ const toolbar = {
this.preferencesInputs = this.preferencesInputs || {};
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(
'Edge Loop Promotion Segments',
@ -1238,6 +1287,36 @@ const toolbar = {
'0.01',
'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');
actions.className = 'doc-dialog-actions';
const defaultsBtn = this.addButton(actions, 'Defaults', () => {
@ -1251,7 +1330,13 @@ const toolbar = {
edgeSelectedLineWidth: Number(this.preferencesInputs?.edgeSelectedLineWidth?.value),
sketchArcSegmentLength: Number(this.preferencesInputs?.sketchArcSegmentLength?.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 });
});
applyBtn.classList.add('compact');