refactor(editor): delete editingHole + activeHandleDrag legacy flags

Migrate the two pure-mirror interaction flags off `useEditor` onto the
authoritative `useInteractionScope`. Both carried payloads byte-identical to
the scope union, so this is a zero-behaviour-change refactor: readers use the
reference-stable `useEditingHole()` / `useActiveHandleDrag()` hooks (or the
`getEditingHole()` imperative read), and producers drive the scope directly
with guarded `endIf` so clearing one interaction never stomps an unrelated
scope. Adds the `holeEditScope` builder + a no-leaked-flag invariant test.

Closes the first slice of the legacy-flag deletion; the rich-payload flags
(movingNode, curving*, *Endpoint, placementDragMode) remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-23 11:30:16 -04:00
co-authored by Claude Opus 4.8
parent f773e6b8c5
commit 2f2c3ef8f9
21 changed files with 273 additions and 119 deletions
+33 -15
View File
@@ -4,11 +4,14 @@ import { type AnyNode, type CeilingNode, useScene } from '@pascal-app/core'
import {
ActionButton,
ActionGroup,
holeEditScope,
PanelSection,
PanelWrapper,
SliderControl,
triggerSFX,
useEditingHole,
useEditor,
useInteractionScope,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
@@ -25,8 +28,7 @@ import { useCallback, useEffect, useRef } from 'react'
export function CeilingPanel() {
const selectedId = useViewer((s) => s.selection.selectedIds[0])
const setSelection = useViewer((s) => s.setSelection)
const editingHole = useEditor((s) => s.editingHole)
const setEditingHole = useEditor((s) => s.setEditingHole)
const editingHole = useEditingHole()
const setMovingNode = useEditor((s) => s.setMovingNode)
const node = useScene((s) =>
@@ -48,20 +50,26 @@ export function CeilingPanel() {
const handleClose = useCallback(() => {
setSelection({ selectedIds: [] })
setEditingHole(null)
}, [setSelection, setEditingHole])
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}, [setSelection])
useEffect(() => {
if (!node) {
setEditingHole(null)
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}
}, [node, setEditingHole])
}, [node])
useEffect(() => {
return () => {
setEditingHole(null)
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}
}, [setEditingHole])
}, [])
const handleAddHole = useCallback(() => {
if (!(node && selectedId)) return
@@ -91,15 +99,17 @@ export function CeilingPanel() {
holes: [...currentHoles, newHole],
holeMetadata: [...currentMetadata, { source: 'manual' }],
})
setEditingHole({ nodeId: selectedId, holeIndex: currentHoles.length })
}, [node, selectedId, handleUpdate, setEditingHole])
useInteractionScope
.getState()
.begin(holeEditScope({ nodeId: selectedId, holeIndex: currentHoles.length }))
}, [node, selectedId, handleUpdate])
const handleEditHole = useCallback(
(index: number) => {
if (!selectedId) return
setEditingHole({ nodeId: selectedId, holeIndex: index })
useInteractionScope.getState().begin(holeEditScope({ nodeId: selectedId, holeIndex: index }))
},
[selectedId, setEditingHole],
[selectedId],
)
const handleDeleteHole = useCallback(
@@ -114,10 +124,12 @@ export function CeilingPanel() {
const newMetadata = currentMetadata.filter((_, i) => i !== index)
handleUpdate({ holes: newHoles, holeMetadata: newMetadata })
if (editingHole?.nodeId === selectedId && editingHole?.holeIndex === index) {
setEditingHole(null)
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}
},
[selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole, setEditingHole],
[selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole],
)
const handleMove = useCallback(() => {
@@ -213,7 +225,13 @@ export function CeilingPanel() {
<ActionButton
className="h-7 bg-primary text-primary-foreground hover:bg-primary/90"
label="Done"
onClick={() => setEditingHole(null)}
onClick={() =>
useInteractionScope
.getState()
.endIf(
(scope) => scope.kind === 'reshaping' && scope.reshape === 'hole',
)
}
/>
) : isAutoHole ? (
<div className="rounded-md bg-[#2C2C2E] px-2 py-1 text-[10px] text-muted-foreground">
+33 -15
View File
@@ -4,11 +4,14 @@ import { type AnyNode, type SlabNode, useScene } from '@pascal-app/core'
import {
ActionButton,
ActionGroup,
holeEditScope,
PanelSection,
PanelWrapper,
SliderControl,
triggerSFX,
useEditingHole,
useEditor,
useInteractionScope,
} from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
@@ -28,8 +31,7 @@ import { useCallback, useEffect, useRef } from 'react'
export function SlabPanel() {
const selectedId = useViewer((s) => s.selection.selectedIds[0])
const setSelection = useViewer((s) => s.setSelection)
const editingHole = useEditor((s) => s.editingHole)
const setEditingHole = useEditor((s) => s.setEditingHole)
const editingHole = useEditingHole()
const setMovingNode = useEditor((s) => s.setMovingNode)
const node = useScene((s) =>
@@ -52,20 +54,26 @@ export function SlabPanel() {
const handleClose = useCallback(() => {
setSelection({ selectedIds: [] })
setEditingHole(null)
}, [setSelection, setEditingHole])
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}, [setSelection])
useEffect(() => {
if (!node) {
setEditingHole(null)
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}
}, [node, setEditingHole])
}, [node])
useEffect(() => {
return () => {
setEditingHole(null)
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}
}, [setEditingHole])
}, [])
const handleAddHole = useCallback(() => {
if (!(node && selectedId)) return
@@ -95,15 +103,17 @@ export function SlabPanel() {
holes: [...currentHoles, newHole],
holeMetadata: [...currentMetadata, { source: 'manual' }],
})
setEditingHole({ nodeId: selectedId, holeIndex: currentHoles.length })
}, [node, selectedId, handleUpdate, setEditingHole])
useInteractionScope
.getState()
.begin(holeEditScope({ nodeId: selectedId, holeIndex: currentHoles.length }))
}, [node, selectedId, handleUpdate])
const handleEditHole = useCallback(
(index: number) => {
if (!selectedId) return
setEditingHole({ nodeId: selectedId, holeIndex: index })
useInteractionScope.getState().begin(holeEditScope({ nodeId: selectedId, holeIndex: index }))
},
[selectedId, setEditingHole],
[selectedId],
)
const handleDeleteHole = useCallback(
@@ -118,10 +128,12 @@ export function SlabPanel() {
const newMetadata = currentMetadata.filter((_, i) => i !== index)
handleUpdate({ holes: newHoles, holeMetadata: newMetadata })
if (editingHole?.nodeId === selectedId && editingHole?.holeIndex === index) {
setEditingHole(null)
useInteractionScope
.getState()
.endIf((scope) => scope.kind === 'reshaping' && scope.reshape === 'hole')
}
},
[selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole, setEditingHole],
[selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole],
)
const handleMove = useCallback(() => {
@@ -219,7 +231,13 @@ export function SlabPanel() {
<ActionButton
className="h-7 bg-primary text-primary-foreground hover:bg-primary/90"
label="Done"
onClick={() => setEditingHole(null)}
onClick={() =>
useInteractionScope
.getState()
.endIf(
(scope) => scope.kind === 'reshaping' && scope.reshape === 'hole',
)
}
/>
) : isAutoHole ? (
<div className="rounded-md bg-[#2C2C2E] px-2 py-1 text-[10px] text-muted-foreground">