Skip to content

Commit 2b9e75e

Browse files
committed
checkpoint boundary selection work
1 parent d7c76f8 commit 2b9e75e

11 files changed

Lines changed: 75 additions & 90 deletions

File tree

docs/agents.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,7 @@ src/
220220
- Known regression history: commit `5093eec4` introduced an overly permissive derived-edge proximity gate (`segLen * 0.35`) in `resolveDerivedEdgeCandidate`; this causes incorrect face/edge picks in sketch derive hover. Keep tight gate (`2.5`) unless replaced with a screen-space metric.
221221
- Geometry graph refactor plan is tracked in `docs/void-geomgraph-plan.md` (surfaces + boundaries as canonical entities; solids as derived artifacts).
222222
- Phase 0 scaffolding status:
223-
- new `GeometryStore` API is wired in parallel (no behavior change yet)
224-
- feature-flag rollout key is `void.geomGraphV2` (stored in preferences/admin)
223+
- new `GeometryStore` API is wired as the single active path (no rollout flags)
225224
- Phase 1 in-progress status:
226225
- surface/profile/edge hover ranking logic has been extracted into `src/void/interact/selection_resolver.js`
227226
- `src/void/interact/planes.js#getPrimarySurfaceHitFromIntersections()` is now a thin delegate to the resolver

docs/void-geomgraph-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
4. Phase 3: Route extrude inputs to `region_id`.
129129
5. Phase 4: Route chamfer inputs to `segment_id`.
130130
6. Phase 5: Remove legacy face/edge ad-hoc paths.
131-
7. Use feature flag `void.geomGraphV2` during rollout.
131+
7. Forward-only: remove rollout toggles and legacy branching once parity is reached.
132132

133133
## Performance and Worker Plan
134134
1. Keep all heavy geometry graph and topology steps in worker.

src/void/api/geometry_store.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ function createGeometryStoreApi(getApi) {
44
return {
55
schemaVersion: 1,
66
state: null,
7-
flags: {
8-
geomGraphV2: false
9-
},
107

118
defaultState() {
129
return {
@@ -94,14 +91,6 @@ function createGeometryStoreApi(getApi) {
9491
state.meta.feature_count = Number(snapshot?.meta?.feature_count) || state.meta.feature_count || 0;
9592
state.meta.generated_at = Date.now();
9693
return state;
97-
},
98-
99-
setFeatureFlags(flags = {}) {
100-
this.flags.geomGraphV2 = !!flags.geomGraphV2;
101-
},
102-
103-
isGeomGraphV2Enabled() {
104-
return !!this.flags.geomGraphV2;
10594
}
10695
};
10796
}

src/void/api/solids.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const SOLID_CREASE_ANGLE_DEG = 30;
1010

1111
function createSolidsApi(getApi) {
1212
function resolveProfileTargetRef(profileTarget = {}) {
13-
const regionId = String(profileTarget?.region_id || profileTarget?.regionId || '');
14-
const match = regionId.match(/^(?:region:)?profile:([^:]+):([^:]+)$/);
13+
const regionId = String(profileTarget?.region_id || '');
14+
const match = regionId.match(/^profile:([^:]+):([^:]+)$/);
1515
if (!match) return { regionId: null, sketchId: null, profileId: null, key: null };
1616
const sketchId = match[1];
1717
const profileId = match[2];

src/void/interact/planes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ function getPrimarySurfaceHitFromIntersections(intersections) {
430430
: (sketchEditing
431431
? (retargetMode ? SELECTION_MODES.sketchRetarget : SELECTION_MODES.sketch)
432432
: SELECTION_MODES.solid);
433+
const edgeGateDistance = (sketchEditing && !retargetMode && !editingExtrudeProfiles) ? 0.5 : 2.5;
433434
return resolveSelectionCandidate(intersections, {
434435
api,
435436
mode,
@@ -441,7 +442,7 @@ function getPrimarySurfaceHitFromIntersections(intersections) {
441442
retargetMode,
442443
editingExtrudeProfiles,
443444
sketchFaceEpsilon: 0.25,
444-
edgeGateDistance: 2.5
445+
edgeGateDistance
445446
});
446447
}
447448

src/void/properties.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const PANEL_MIN_LEFT = 10;
1212
const PANEL_MIN_TOP = 60;
1313

1414
function resolveExtrudeProfileRef(profile = {}) {
15-
const regionId = String(profile?.region_id || profile?.regionId || '');
16-
const match = regionId.match(/^(?:region:)?profile:([^:]+):([^:]+)$/);
15+
const regionId = String(profile?.region_id || '');
16+
const match = regionId.match(/^profile:([^:]+):([^:]+)$/);
1717
if (!match) return { regionId: null, sketchId: null, profileId: null, key: null };
1818
const sketchId = match[1];
1919
const profileId = match[2];

src/void/sketch/geometry.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ function resolveDerivedEdgeCandidate(event, intersections, feature) {
283283
if (!solidId) return null;
284284
const faceKey = Number.isFinite(faceId) ? `${solidId}:${faceId}` : null;
285285

286-
// Promote dense closed loops (typically circles) to full-loop edge derive.
286+
// Promote short-segment hits to their boundary loop (polyline semantics),
287+
// while long segments remain individually selectable.
287288
if (!edge?.pathWorld && edgeKey.startsWith('faceedge:')) {
288289
const parts = edgeKey.split(':');
289290
const segIndex = Number(parts[parts.length - 1]);
@@ -292,25 +293,12 @@ function resolveDerivedEdgeCandidate(event, intersections, feature) {
292293
if (sid && Number.isFinite(fid) && Number.isFinite(segIndex)) {
293294
const loops = api.solids?.getFaceBoundaryLoops?.(`${sid}:${fid}`) || [];
294295
const loop = loops.find(lp => Array.isArray(lp?.segmentIndices) && lp.segmentIndices.includes(segIndex) && lp?.closed);
295-
if (loop && Array.isArray(loop.points) && loop.points.length >= 8) {
296-
const center = new THREE.Vector3();
297-
for (const p of loop.points) center.add(p);
298-
center.multiplyScalar(1 / loop.points.length);
299-
let mean = 0;
300-
const rr = [];
301-
for (const p of loop.points) {
302-
const r = p.distanceTo(center);
303-
rr.push(r);
304-
mean += r;
305-
}
306-
mean /= Math.max(1, rr.length);
307-
let varR = 0;
308-
for (const r of rr) {
309-
const d = r - mean;
310-
varR += d * d;
311-
}
312-
const rel = mean > 1e-8 ? Math.sqrt(varR / Math.max(1, rr.length)) / mean : 1;
313-
if (rel <= 0.12) {
296+
if (loop && Array.isArray(loop.points) && loop.points.length >= 3) {
297+
const segs = api.solids?.getFaceBoundarySegments?.(`${sid}:${fid}`) || [];
298+
const seg = segs[segIndex];
299+
const hoveredLen = seg?.a && seg?.b ? seg.a.distanceTo(seg.b) : Infinity;
300+
const shortSegThreshold = 6;
301+
if (Number.isFinite(hoveredLen) && hoveredLen <= shortSegThreshold) {
314302
edge = {
315303
...edge,
316304
key: `faceedgeloop:${sid}:${fid}:${loops.indexOf(loop)}`,
@@ -439,6 +427,23 @@ function resolveDerivedEdgeCandidate(event, intersections, feature) {
439427
const localA = toFaceLocal(a);
440428
const localB = toFaceLocal(b);
441429
const localP = hoverWorld ? toFaceLocal(new THREE.Vector3(hoverWorld.x, hoverWorld.y, hoverWorld.z)) : null;
430+
const pathWorldSegments = [];
431+
const pathLocalSegments = [];
432+
if (Array.isArray(edge?.pathWorld) && edge.pathWorld.length >= 2) {
433+
for (let i = 0; i + 1 < edge.pathWorld.length; i++) {
434+
const wa = edge.pathWorld[i];
435+
const wb = edge.pathWorld[i + 1];
436+
if (!wa || !wb) continue;
437+
const la = this.worldToSketchLocal(wa, basis);
438+
const lb = this.worldToSketchLocal(wb, basis);
439+
if (!la || !lb) continue;
440+
pathLocalSegments.push({ a: la, b: lb });
441+
pathWorldSegments.push({
442+
a: { x: Number(wa.x || 0), y: Number(wa.y || 0), z: Number(wa.z || 0) },
443+
b: { x: Number(wb.x || 0), y: Number(wb.y || 0), z: Number(wb.z || 0) }
444+
});
445+
}
446+
}
442447
const canonicalEntity = resolvedPrimary?.entity || edgeHit?.entity || null;
443448
const canonicalEntityId = String(canonicalEntity?.id || '');
444449
const canonicalEntityKind = String(canonicalEntity?.kind || 'boundary-segment');
@@ -454,6 +459,8 @@ function resolveDerivedEdgeCandidate(event, intersections, feature) {
454459
aWorld: { x: a.x, y: a.y, z: a.z },
455460
bWorld: { x: b.x, y: b.y, z: b.z },
456461
midWorld: { x: mid.x, y: mid.y, z: mid.z },
462+
pathLocalSegments: pathLocalSegments.length ? pathLocalSegments : null,
463+
pathWorldSegments: pathWorldSegments.length ? pathWorldSegments : null,
457464
a: { x: a.x, y: a.y, z: a.z },
458465
b: { x: b.x, y: b.y, z: b.z },
459466
hoverPoint: hoverPoint ? { ...hoverPoint, world: hoverWorld } : null,

src/void/sketch/index.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ function updateSketchInteractionVisuals() {
193193
&& !this.sketchRectStart;
194194
const externalPointLocal = canShowExternalPreview ? (external?.hoverPoint?.local || null) : null;
195195
const showExternalPoint = !!externalPointLocal;
196-
const showExternalLine = canShowExternalPreview && !!external?.aLocal && !!external?.bLocal;
196+
const showExternalSegments = canShowExternalPreview && Array.isArray(external?.pathLocalSegments) && external.pathLocalSegments.length > 1;
197+
const showExternalLine = canShowExternalPreview && !showExternalSegments && !!external?.aLocal && !!external?.bLocal;
197198
const externalLine = showExternalLine
198199
? { a: external.aLocal, b: external.bLocal, forceHover: true, projected: true }
199200
: null;
@@ -202,10 +203,14 @@ function updateSketchInteractionVisuals() {
202203
: null;
203204
const externalEnd = null;
204205
const externalPointWorld = showExternalPoint ? (external?.hoverPoint?.world || null) : null;
205-
const projectedFaceSegments = !showExternalPoint && !showExternalLine && this.hoveredSolidFaceKey
206+
const projectedFaceSegments = showExternalSegments
207+
? external.pathLocalSegments
208+
: (!showExternalPoint && !showExternalLine && this.hoveredSolidFaceKey
206209
? this.projectFaceBoundaryToSketch(feature, this.hoveredSolidFaceKey)
207-
: null;
208-
const sourceFaceWorldSegments = !showExternalPoint && !showExternalLine && this.hoveredSolidFaceKey
210+
: null);
211+
const sourceFaceWorldSegments = showExternalSegments
212+
? (external.pathWorldSegments || null)
213+
: (!showExternalPoint && !showExternalLine && this.hoveredSolidFaceKey
209214
? (() => {
210215
const faceSegments = api.solids?.getFaceBoundarySegments?.(this.hoveredSolidFaceKey) || [];
211216
if (!faceSegments.length) return null;
@@ -221,7 +226,7 @@ function updateSketchInteractionVisuals() {
221226
}
222227
return out.length ? out : null;
223228
})()
224-
: null;
229+
: null);
225230
api.sketchRuntime?.setEntityInteraction(feature.id, {
226231
hoveredId: this.sketchDrag ? dragHoverId : this.hoveredSketchEntityId,
227232
selectedIds: Array.from(this.selectedSketchEntities),

src/void/sketch/tools.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -436,12 +436,31 @@ function useHoveredDerivedEdge() {
436436
if (!selectedEdges.length && !selectedPoints.length && !selectedFaces.length) {
437437
// `u` should prioritize the actively hovered derived edge candidate.
438438
if (hovered?.aLocal && hovered?.bLocal) {
439-
selectedEdges.push({
440-
type: 'edge',
441-
aLocal: hovered.aLocal,
442-
bLocal: hovered.bLocal,
443-
source: hovered.source || null
444-
});
439+
if (Array.isArray(hovered?.pathLocalSegments) && hovered.pathLocalSegments.length > 1) {
440+
const worldSegs = Array.isArray(hovered?.pathWorldSegments) ? hovered.pathWorldSegments : [];
441+
for (let i = 0; i < hovered.pathLocalSegments.length; i++) {
442+
const seg = hovered.pathLocalSegments[i];
443+
const wseg = worldSegs[i] || null;
444+
if (!seg?.a || !seg?.b) continue;
445+
selectedEdges.push({
446+
type: 'edge',
447+
aLocal: seg.a,
448+
bLocal: seg.b,
449+
source: {
450+
...(hovered.source || {}),
451+
a: wseg?.a || hovered?.source?.a || null,
452+
b: wseg?.b || hovered?.source?.b || null
453+
}
454+
});
455+
}
456+
} else {
457+
selectedEdges.push({
458+
type: 'edge',
459+
aLocal: hovered.aLocal,
460+
bLocal: hovered.bLocal,
461+
source: hovered.source || null
462+
});
463+
}
445464
} else if (this.hoveredSolidFaceKey) {
446465
// Face derive is fallback only when no discrete edge is hovered.
447466
selectedFaces.push(this.hoveredSolidFaceKey);

src/void/solid/rebuild.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { applyChamferFeature } from './chamfer.js';
88
const CLIPPER_SCALE = 100000;
99

1010
function resolveProfileTargetRef(profileTarget = {}) {
11-
const regionId = String(profileTarget?.region_id || profileTarget?.regionId || '');
12-
const match = regionId.match(/^(?:region:)?profile:([^:]+):([^:]+)$/);
11+
const regionId = String(profileTarget?.region_id || '');
12+
const match = regionId.match(/^profile:([^:]+):([^:]+)$/);
1313
if (!match) return { regionId: null, sketchId: null, profileId: null, key: null };
1414
const sketchId = match[1];
1515
const profileId = match[2];

0 commit comments

Comments
 (0)