feat(editor): wall room/single + fence continuous/single chain toggles

Replace the legacy held-Alt mechanism on wall and fence drafting with a
mode toggle, mirroring the snapping-mode chip:

- Wall: `wallChainMode` room (auto-close on loop) / single. Room mode
  finishes automatically when the new endpoint lands within the join-snap
  radius of the chain's first vertex; single commits one wall per click.
- Fence: `fenceChainMode` continuous (chain until double-click/Esc) /
  single. Fences are linear barriers, so continuous has no auto-close.
- Both: Alt-tap cycles the active drafting tool's chain mode (clean-tap,
  scoped to wall/fence drafting); a clickable HUD chip shows the mode.
  Persisted + migrated in `useEditor`.

Migrate wall and fence off held-Alt-bypass-alignment to the unified
convention: alignment now follows the magnetic snap mode, which frees Alt
for the toggle. 2D floorplan parity kept in sync with the 3D tools.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-26 11:27:12 -04:00
co-authored by Claude Opus 4.8
parent 6d02ad424e
commit 2666b07479
10 changed files with 281 additions and 46 deletions
+12 -5
View File
@@ -467,8 +467,7 @@ export const FenceTool: React.FC = () => {
}
// Align the drafted point onto another object's nearest real anchor and
// publish the guide. Alt bypasses alignment. Returns the possibly snapped
// point.
// publish the guide. Returns the possibly snapped point.
const alignPoint = (point: FencePlanPoint, bypass: boolean): FencePlanPoint => {
// Figma alignment pulls the endpoint onto existing corners / edges, so it
// is a line snap — suppress it whenever magnetic snap is off (`'off'` /
@@ -500,8 +499,9 @@ export const FenceTool: React.FC = () => {
const localPoint: FencePlanPoint = [event.localPosition[0], event.localPosition[2]]
// While drafting, the segment locks to 15° rays from its start.
// Snapping is governed by the snapping mode (`'off'` is the bypass);
// there is no Shift hold-to-bypass. Alt still bypasses alignment guides.
const bypassAlign = event.nativeEvent?.altKey === true
// there is no Shift hold-to-bypass. Alignment follows the magnetic snap
// mode, not Alt (Alt-tap toggles continuous/single chaining).
const bypassAlign = !isMagneticSnapActive()
if (buildingState.current === 1) {
const angleLocked = isAngleSnapActive()
@@ -567,7 +567,7 @@ export const FenceTool: React.FC = () => {
const { walls, fences } = getCurrentLevelElements()
const localClick: FencePlanPoint = [event.localPosition[0], event.localPosition[2]]
const bypassAlign = event.nativeEvent?.altKey === true
const bypassAlign = !isMagneticSnapActive()
if (buildingState.current === 0) {
const snappedStart = alignPoint(
@@ -612,6 +612,13 @@ export const FenceTool: React.FC = () => {
refreshAlignmentCandidates()
useAlignmentGuides.getState().clear()
// Single mode commits one segment per click: stop drafting so the next
// click starts a fresh segment instead of chaining off this endpoint.
if (useEditor.getState().fenceChainMode === 'single') {
stopDrafting()
return
}
const nextStart = createdFence.end
// Publish the resolved chain start so the 2D floor-plan draft
// chains its next segment from the same point (its own snap