Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions src/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import { capturePathClick } from '$lib/pathCapture';
import { surfaceSnap, dropToSurface } from '$lib/snapping';
import { editingObject, exitEditMode, raycastHandles, clearVertexSelection, onProxyMoved, onProxyDragChanged, tickMeshEdit } from '$lib/meshEdit';
import { faceEditObject, faceEditOp, commitArmedFaceOp, exitFaceEdit, highlightFaceByTriangle, attachFaceGizmo, detachFaceGizmo, onFaceGizmoMoved, onFaceGizmoDragChanged, autoApplyFaceOp, faceEditMulti, toggleFaceSelection, clearFaceSelection, lookupEditable } from '$lib/faceEdit';
import { faceEditObject, faceEditOp, commitArmedFaceOp, exitFaceEdit, highlightFaceByTriangle, attachFaceGizmo, detachFaceGizmo, onFaceGizmoMoved, onFaceGizmoDragChanged, autoApplyFaceOp, faceEditMulti, toggleFaceSelection, clearFaceSelection, pickFaceUnit, lookupEditable } from '$lib/faceEdit';
import { fireObjectClick } from '$lib/flowRuntime';
import { initVRControls, updateVRControls, raycastMenu, raycastPanel, raycastPalette, raycastProps, raycastPrefabs, raycastKeyboard, raycastChat, raycastEdit, raycastSnap, raycastSettings, raycastApprove, placePrefabGhost, vrFaceTrigger, vrVertexTrigger, vrVertexGrabStart, vrVertexGrabEnd, beginStretchSliderDrag, endStretchSliderDrag, executeVRMenuAction, resetWorldRig, onInputSourcesChange, worldToContentPose, boxSelectStart, boxSelectEnd, boxSelectActive, applyVRFrameRate, shouldSendHands, onHandPinchStart, onHandPinchEnd, pinchMenuToggledAt, firePingIfArmed, vrModuleTriggerStart, vrModuleTriggerEnd, vrModuleSelectSwallowed } from '$lib/vrControls';
import { vrKeyboardTarget } from '$lib/vrKeyboard';
Expand Down Expand Up @@ -590,19 +590,33 @@
const edited = lookupEditable($faceEditObject);
const hit = edited ? selectionRaycaster.intersectObject(edited, false)[0] : null;
const tri = hit && hit.faceIndex != null ? hit.faceIndex : -1;
highlightFaceByTriangle(tri);
// 212: Multi mode accumulates picks (the op button applies to the set);
// otherwise 176 auto-applies the active extrude/inset on the click
// E10: ctrl/shift-click ADDS to the selection (never auto-applies); a
// plain click REPLACES it with the unit under the cursor. The heal
// flag is off for additive clicks — the heal would wipe the very
// selection they are adding to.
const additive = event.ctrlKey || event.shiftKey || event.metaKey || $faceEditMulti;
highlightFaceByTriangle(tri, !additive);
if (tri >= 0) {
if ($faceEditMulti) toggleFaceSelection(tri);
else autoApplyFaceOp();
} else clearFaceSelection(); // D2: a miss drops the accumulated multi-pick
// B1 (inset fix): a seated MOVE gizmo intercepts the NEXT click (the
// dragging||axis guard above skips face dispatch), so click 2 of an
// armed inset/extrude DRAGGED the face instead. Only the Move op
// seats the gizmo; a miss still detaches it.
if ($faceEditOp === 'move' || tri < 0) attachFaceGizmo();
else detachFaceGizmo();
if (additive) {
toggleFaceSelection(tri);
// B1: only the armed Move op keeps a gizmo on a non-commit click
if ($faceEditOp === 'move') attachFaceGizmo();
else detachFaceGizmo();
} else {
pickFaceUnit(tri);
const committed = autoApplyFaceOp();
// E7: a commit re-seats the gizmo itself (on the new cap);
// otherwise the B1 rule holds — a seated gizmo intercepts the
// NEXT click, so only Move keeps one armed
if (!committed) {
if ($faceEditOp === 'move') attachFaceGizmo();
else detachFaceGizmo();
}
}
} else {
clearFaceSelection(); // D2: a miss drops the accumulated multi-pick
attachFaceGizmo(); // no target left -> detaches
}
return;
}
// light pick-proxies select their light (lights have no raycastable geometry)
Expand Down
57 changes: 41 additions & 16 deletions src/components/menu/MeshEditPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
commitFaceOp,
faceEditGranularity,
setFaceGranularity,
faceEditMulti,
faceEditSelectedTris,
toggleFaceMulti,
faceSelectionInfo,
faceGizmoSpace,
meshEditWireframe,
meshEditHotkeys
} from '$lib/faceEdit';
Expand Down Expand Up @@ -95,21 +95,24 @@
}
];

/** a target exists for a one-shot op (multi selection or a picked unit) */
/** a target exists for a one-shot op (E10: the selection first, else a picked unit) */
function hasTarget() {
if ($faceEditMulti && $faceEditSelectedTris.length) return true;
if ($faceEditSelectedTris.length) return true;
if (($faceEditGranularity === 'face' ? $faceEditHighlight : $faceEditHoverTri) >= 0) return true;
return false;
}

// E10: live counts — selected faces/tris + boundary-edge counts when exactly
// two faces are picked (a bridge mismatch shows BEFORE clicking Bridge)
const selInfo = $derived.by(() => {
void $faceEditSelectedTris; // the trigger; the geometry poke rides objectsGroup
void $objectsGroup;
return faceSelectionInfo();
});

/** @param {string} op */
function runOp(op) {
const spec = OPS.find((o) => o.op === op);
// 212: Multi mode — the op button applies to the whole accumulated selection
if ($faceEditMulti && $faceEditSelectedTris.length) {
commitFaceOp(/** @type {any} */ (op), $faceEditAmount);
return;
}
if (op === 'bridge') {
commitFaceOp('bridge', 0); // validates the two-face selection + toasts
return;
Expand Down Expand Up @@ -237,13 +240,15 @@
>
{/each}
</div>
<button
id="mesh-multi"
class="rounded-full px-2.5 py-1 text-xs {$faceEditMulti ? 'bg-primary-600 text-white' : 'bg-gray-700 hover:bg-gray-600'}"
title="Accumulate several picks, then apply an op to all"
onclick={() => toggleFaceMulti()}
>Multi{$faceEditMulti && $faceEditSelectedTris.length ? ` (${$faceEditSelectedTris.length})` : ''}</button
>
<!-- E10: Multi button retired — ctrl-click always adds; live counts here -->
<span id="mesh-sel-counts" class="text-[11px] text-gray-400" title="Selected faces · triangles (Ctrl+click adds)">
{selInfo.faces} face{selInfo.faces === 1 ? '' : 's'} · {selInfo.tris} tri{selInfo.tris === 1 ? '' : 's'}{#if selInfo.loops}<span
class={selInfo.loops[0] === selInfo.loops[1] ? '' : 'text-red-400'}
title="Boundary edges of the two selected faces — Bridge needs them EQUAL"
>
· {selInfo.loops[0]} ↔ {selInfo.loops[1]} edges</span
>{/if}
</span>

<span class="h-5 w-px shrink-0 bg-gray-600/70"></span>

Expand All @@ -263,6 +268,26 @@
>
{/each}
</div>

<span class="h-5 w-px shrink-0 bg-gray-600/70"></span>

<!-- E9: gizmo orientation (Local = face basis, Z along the normal) -->
<div
id="mesh-gizmo-space"
class="flex overflow-hidden rounded-full border border-gray-600 text-xs"
title="Gizmo orientation — Local aligns to the face (Z = its normal). Scale handles always orient local."
>
<button
id="mesh-space-local"
class="px-2 py-0.5 {$faceGizmoSpace === 'local' ? 'bg-primary-600 text-white' : 'bg-gray-700 hover:bg-gray-600'}"
onclick={() => faceGizmoSpace.set('local')}>Local</button
>
<button
id="mesh-space-world"
class="px-2 py-0.5 {$faceGizmoSpace === 'world' ? 'bg-primary-600 text-white' : 'bg-gray-700 hover:bg-gray-600'}"
onclick={() => faceGizmoSpace.set('world')}>World</button
>
</div>
{:else}
<!-- segment: vertex tools (D5: ONE selection — click selects, Ctrl+click
adds, the gizmo on the last pick drags the whole set) -->
Expand Down
Loading