From f6bb8c15696f3cc4c4ae13d3b4bf4bdac373a663 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 2 Mar 2026 14:24:30 -0500 Subject: [PATCH] void update boundary provenance --- docs/void/plan-face-provenance.md | 56 ++++ src/void/api/geometry_store.js | 13 +- src/void/api/solids.js | 445 +++++++++++++++++++++++++++++- src/void/toolbar.js | 91 +++++- 4 files changed, 598 insertions(+), 7 deletions(-) diff --git a/docs/void/plan-face-provenance.md b/docs/void/plan-face-provenance.md index b8ed08c4..794c8cab 100644 --- a/docs/void/plan-face-provenance.md +++ b/docs/void/plan-face-provenance.md @@ -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 diff --git a/src/void/api/geometry_store.js b/src/void/api/geometry_store.js index 76543187..3211e763 100644 --- a/src/void/api/geometry_store.js +++ b/src/void/api/geometry_store.js @@ -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; diff --git a/src/void/api/solids.js b/src/void/api/solids.js index 93722cf2..cb4b9c0d 100644 --- a/src/void/api/solids.js +++ b/src/void/api/solids.js @@ -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; diff --git a/src/void/toolbar.js b/src/void/toolbar.js index de3d9ca4..0555c772 100644 --- a/src/void/toolbar.js +++ b/src/void/toolbar.js @@ -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');