Refactor guide UI state and slab rendering
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import {
|
||||
buildFloorplanItemEntry,
|
||||
buildFloorplanStairEntry as buildSharedFloorplanStairEntry,
|
||||
@@ -67,6 +68,7 @@ import { duplicateRoofSubtree } from '../../lib/roof-duplication'
|
||||
import { sfxEmitter } from '../../lib/sfx-bus'
|
||||
import { duplicateStairSubtree } from '../../lib/stair-duplication'
|
||||
import { cn } from '../../lib/utils'
|
||||
import type { GuideUiState } from '../../store/use-editor'
|
||||
import useEditor from '../../store/use-editor'
|
||||
import { FloorplanActionMenuLayer as Editor2dFloorplanActionMenuLayer } from '../editor-2d/floorplan-action-menu-layer'
|
||||
import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay'
|
||||
@@ -3267,6 +3269,7 @@ const FloorplanGridLayer = memo(function FloorplanGridLayer({
|
||||
})
|
||||
|
||||
const FloorplanGuideLayer = memo(function FloorplanGuideLayer({
|
||||
guideUi,
|
||||
guides,
|
||||
isInteractive,
|
||||
selectedGuideId,
|
||||
@@ -3275,6 +3278,7 @@ const FloorplanGuideLayer = memo(function FloorplanGuideLayer({
|
||||
onGuideSelect,
|
||||
onGuideTranslateStart,
|
||||
}: {
|
||||
guideUi: Record<string, GuideUiState>
|
||||
guides: GuideNode[]
|
||||
isInteractive: boolean
|
||||
selectedGuideId: GuideNode['id'] | null
|
||||
@@ -3303,7 +3307,7 @@ const FloorplanGuideLayer = memo(function FloorplanGuideLayer({
|
||||
activeGuideInteractionGuideId === guide.id ? activeGuideInteractionMode : null
|
||||
}
|
||||
guide={guide}
|
||||
isInteractive={isInteractive && guide.locked !== true}
|
||||
isInteractive={isInteractive && guideUi[guide.id]?.locked !== true}
|
||||
isSelected={selectedGuideId === guide.id}
|
||||
key={guide.id}
|
||||
onGuideSelect={onGuideSelect}
|
||||
@@ -3405,21 +3409,24 @@ function FloorplanReferenceScaleLine({
|
||||
|
||||
function FloorplanReferenceScaleLayer({
|
||||
draft,
|
||||
guideUi,
|
||||
guides,
|
||||
palette,
|
||||
unit,
|
||||
unitsPerPixel,
|
||||
}: {
|
||||
draft: ReferenceScaleDraft | null
|
||||
guideUi: Record<string, GuideUiState>
|
||||
guides: GuideNode[]
|
||||
palette: FloorplanPalette
|
||||
unit: 'metric' | 'imperial'
|
||||
unitsPerPixel: number
|
||||
}) {
|
||||
const visibleReferences = guides
|
||||
.filter((guide) => guideUi[guide.id]?.scaleReferenceVisible !== false)
|
||||
.map((guide) => guide.scaleReference)
|
||||
.filter((reference): reference is NonNullable<GuideNode['scaleReference']> =>
|
||||
Boolean(reference && reference.visible !== false),
|
||||
Boolean(reference),
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -6170,7 +6177,10 @@ export function FloorplanPanel() {
|
||||
const showReferenceFloor = useEditor((s) => s.showReferenceFloor)
|
||||
const referenceFloorOffset = useEditor((s) => s.referenceFloorOffset)
|
||||
const referenceFloorOpacity = useEditor((s) => s.referenceFloorOpacity)
|
||||
const sceneNodes = useScene((state) => state.nodes)
|
||||
const guideUi = useEditor((s) => s.guideUi)
|
||||
const setGuideLocked = useEditor((s) => s.setGuideLocked)
|
||||
const setGuideScaleReferenceVisible = useEditor((s) => s.setGuideScaleReferenceVisible)
|
||||
const clearGuideUi = useEditor((s) => s.clearGuideUi)
|
||||
const [floorplanMarqueeState, setFloorplanMarqueeState] = useState<FloorplanMarqueeState | null>(
|
||||
null,
|
||||
)
|
||||
@@ -6775,24 +6785,33 @@ export function FloorplanPanel() {
|
||||
|
||||
return lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null
|
||||
}, [floorplanLevels, levelNode, referenceFloorOffset, showReferenceFloor])
|
||||
const referenceFloorDescendants = useScene(
|
||||
useShallow((state) => {
|
||||
if (!referenceFloorLevel) {
|
||||
return [] as AnyNode[]
|
||||
}
|
||||
|
||||
return collectLevelDescendants(
|
||||
referenceFloorLevel,
|
||||
state.nodes as Record<string, AnyNode>,
|
||||
).filter((node) => node.visible !== false)
|
||||
}),
|
||||
)
|
||||
const referenceFloorData = useMemo<ReferenceFloorData | null>(() => {
|
||||
if (!referenceFloorLevel) {
|
||||
return null
|
||||
}
|
||||
|
||||
const children = referenceFloorLevel.children
|
||||
.map((childId) => sceneNodes[childId])
|
||||
.filter((node): node is AnyNode => Boolean(node && node.visible !== false))
|
||||
const children = referenceFloorDescendants.filter(
|
||||
(node) => node.parentId === referenceFloorLevel.id,
|
||||
)
|
||||
const referenceWalls = children.filter((node): node is WallNode => node.type === 'wall')
|
||||
const referenceFences = children.filter((node): node is FenceNode => node.type === 'fence')
|
||||
const referenceSlabs = children.filter((node): node is SlabNode => node.type === 'slab')
|
||||
const referenceCeilings = children.filter(
|
||||
(node): node is CeilingNode => node.type === 'ceiling',
|
||||
)
|
||||
const referenceDescendants = collectLevelDescendants(
|
||||
referenceFloorLevel,
|
||||
sceneNodes as Record<string, AnyNode>,
|
||||
).filter((node) => node.visible !== false)
|
||||
const referenceDescendants = referenceFloorDescendants
|
||||
const referenceDescendantById = new Map(referenceDescendants.map((node) => [node.id, node]))
|
||||
|
||||
const referenceFloorplanWalls = referenceWalls.map(getFloorplanWall)
|
||||
@@ -6931,7 +6950,7 @@ export function FloorplanPanel() {
|
||||
slabPolygons,
|
||||
wallPolygons,
|
||||
}
|
||||
}, [referenceFloorLevel, sceneNodes])
|
||||
}, [referenceFloorDescendants, referenceFloorLevel])
|
||||
const hasPendingItemMeshFootprints = floorplanItemEntries.some((entry) => !entry.usesRealMesh)
|
||||
const floorplanStairEntries = useMemo(
|
||||
() =>
|
||||
@@ -8729,13 +8748,14 @@ export function FloorplanPanel() {
|
||||
|
||||
setReferenceScaleDraft((current) => (current?.guideId === payload.guideId ? null : current))
|
||||
setPendingReferenceScale((current) => (current?.guideId === payload.guideId ? null : current))
|
||||
clearGuideUi(payload.guideId)
|
||||
}
|
||||
|
||||
emitter.on('guide:deleted', handleDeleted)
|
||||
return () => {
|
||||
emitter.off('guide:deleted', handleDeleted)
|
||||
}
|
||||
}, [])
|
||||
}, [clearGuideUi])
|
||||
|
||||
const handleReferenceScaleConfirm = useCallback(() => {
|
||||
if (!pendingReferenceScale) {
|
||||
@@ -8784,7 +8804,6 @@ export function FloorplanPanel() {
|
||||
updateNode(
|
||||
pendingReferenceScale.guideId as AnyNodeId,
|
||||
{
|
||||
locked: true,
|
||||
position: nextGuidePosition,
|
||||
scale: nextGuideScale,
|
||||
scaleReference: {
|
||||
@@ -8794,10 +8813,11 @@ export function FloorplanPanel() {
|
||||
measuredLengthUnits: scaledMeasuredLengthUnits,
|
||||
metersPerUnit,
|
||||
label: formatReferenceScaleLabel(displayLength, referenceScaleUnit),
|
||||
visible: true,
|
||||
},
|
||||
} as Partial<GuideNode>,
|
||||
)
|
||||
setGuideLocked(pendingReferenceScale.guideId, true)
|
||||
setGuideScaleReferenceVisible(pendingReferenceScale.guideId, true)
|
||||
setSelectedReferenceId(pendingReferenceScale.guideId)
|
||||
setPendingReferenceScale(null)
|
||||
}, [
|
||||
@@ -8805,6 +8825,8 @@ export function FloorplanPanel() {
|
||||
pendingReferenceScale,
|
||||
referenceScaleUnit,
|
||||
referenceScaleValue,
|
||||
setGuideLocked,
|
||||
setGuideScaleReferenceVisible,
|
||||
setSelectedReferenceId,
|
||||
updateNode,
|
||||
])
|
||||
@@ -11703,7 +11725,7 @@ export function FloorplanPanel() {
|
||||
corner: GuideCorner,
|
||||
event: ReactPointerEvent<SVGCircleElement>,
|
||||
) => {
|
||||
if (event.button !== 0 || !canInteractWithGuides || guide.locked === true) {
|
||||
if (event.button !== 0 || !canInteractWithGuides || guideUi[guide.id]?.locked === true) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11762,7 +11784,7 @@ export function FloorplanPanel() {
|
||||
guideTransformDraftRef.current = nextDraft
|
||||
setGuideTransformDraft(nextDraft)
|
||||
},
|
||||
[canInteractWithGuides, handleGuideSelect, theme],
|
||||
[canInteractWithGuides, guideUi, handleGuideSelect, theme],
|
||||
)
|
||||
const handleGuideTranslateStart = useCallback(
|
||||
(guide: GuideNode, event: ReactPointerEvent<SVGRectElement>) => {
|
||||
@@ -11770,7 +11792,7 @@ export function FloorplanPanel() {
|
||||
event.button !== 0 ||
|
||||
!canInteractWithGuides ||
|
||||
selectedGuideId !== guide.id ||
|
||||
guide.locked === true
|
||||
guideUi[guide.id]?.locked === true
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -11812,7 +11834,7 @@ export function FloorplanPanel() {
|
||||
guideTransformDraftRef.current = nextDraft
|
||||
setGuideTransformDraft(nextDraft)
|
||||
},
|
||||
[canInteractWithGuides, getSvgPointFromClientPoint, selectedGuideId],
|
||||
[canInteractWithGuides, getSvgPointFromClientPoint, guideUi, selectedGuideId],
|
||||
)
|
||||
|
||||
const handleOpeningSelect = useCallback(
|
||||
@@ -14183,6 +14205,7 @@ export function FloorplanPanel() {
|
||||
<FloorplanGuideLayer
|
||||
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
||||
activeGuideInteractionMode={activeGuideInteractionMode}
|
||||
guideUi={guideUi}
|
||||
guides={displayGuides}
|
||||
isInteractive={canInteractWithGuides}
|
||||
onGuideSelect={handleGuideSelect}
|
||||
@@ -14293,6 +14316,7 @@ export function FloorplanPanel() {
|
||||
|
||||
<FloorplanReferenceScaleLayer
|
||||
draft={referenceScaleDraft}
|
||||
guideUi={guideUi}
|
||||
guides={displayGuides}
|
||||
palette={palette}
|
||||
unit={unit}
|
||||
@@ -14545,7 +14569,7 @@ export function FloorplanPanel() {
|
||||
onCornerHoverChange={setHoveredGuideCorner}
|
||||
onCornerPointerDown={handleGuideCornerPointerDown}
|
||||
rotationModifierPressed={rotationModifierPressed}
|
||||
showHandles={canInteractWithGuides && selectedGuide.locked !== true}
|
||||
showHandles={canInteractWithGuides && guideUi[selectedGuide.id]?.locked !== true}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -20,18 +20,22 @@ import { PanelWrapper } from './panel-wrapper'
|
||||
|
||||
type ReferenceNode = ScanNode | GuideNode
|
||||
|
||||
function getScaleStatus(guide: GuideNode) {
|
||||
function getScaleStatus(guide: GuideNode, scaleReferenceVisible: boolean) {
|
||||
const reference = guide.scaleReference
|
||||
if (!reference) {
|
||||
return 'Uncalibrated'
|
||||
}
|
||||
|
||||
return `${reference.visible === false ? 'Scaled (hidden)' : 'Scaled'} · ${reference.label}`
|
||||
return `${scaleReferenceVisible ? 'Scaled' : 'Scaled (hidden)'} · ${reference.label}`
|
||||
}
|
||||
|
||||
export function ReferencePanel() {
|
||||
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
||||
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
||||
const guideUi = useEditor((s) => (selectedReferenceId ? s.guideUi[selectedReferenceId] : undefined))
|
||||
const setGuideLocked = useEditor((s) => s.setGuideLocked)
|
||||
const setGuideScaleReferenceVisible = useEditor((s) => s.setGuideScaleReferenceVisible)
|
||||
const clearGuideUi = useEditor((s) => s.clearGuideUi)
|
||||
const updateNode = useScene((s) => s.updateNode)
|
||||
const deleteNode = useScene((s) => s.deleteNode)
|
||||
const replaceInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -78,13 +82,14 @@ export function ReferencePanel() {
|
||||
url: assetUrl,
|
||||
scaleReference: null,
|
||||
} as Partial<GuideNode>)
|
||||
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
||||
} catch {
|
||||
setReplaceError('Could not replace that image.')
|
||||
} finally {
|
||||
setIsReplacing(false)
|
||||
}
|
||||
},
|
||||
[node?.type, selectedReferenceId, updateNode],
|
||||
[node?.type, selectedReferenceId, setGuideScaleReferenceVisible, updateNode],
|
||||
)
|
||||
|
||||
const handleDeleteGuide = useCallback(() => {
|
||||
@@ -94,8 +99,9 @@ export function ReferencePanel() {
|
||||
|
||||
deleteNode(selectedReferenceId as AnyNode['id'])
|
||||
emitter.emit('guide:deleted', { guideId: selectedReferenceId as GuideNode['id'] })
|
||||
clearGuideUi(selectedReferenceId)
|
||||
setSelectedReferenceId(null)
|
||||
}, [deleteNode, node?.type, selectedReferenceId, setSelectedReferenceId])
|
||||
}, [clearGuideUi, deleteNode, node?.type, selectedReferenceId, setSelectedReferenceId])
|
||||
|
||||
const handleStartScale = useCallback(() => {
|
||||
if (node?.type !== 'guide') {
|
||||
@@ -130,7 +136,9 @@ export function ReferencePanel() {
|
||||
if (!node || (node.type !== 'scan' && node.type !== 'guide')) return null
|
||||
|
||||
const isScan = node.type === 'scan'
|
||||
const scaleStatus = !isScan ? getScaleStatus(node) : null
|
||||
const guideLocked = !isScan && guideUi?.locked === true
|
||||
const scaleReferenceVisible = !isScan && guideUi?.scaleReferenceVisible !== false
|
||||
const scaleStatus = !isScan ? getScaleStatus(node, scaleReferenceVisible) : null
|
||||
|
||||
return (
|
||||
<PanelWrapper
|
||||
@@ -184,14 +192,14 @@ export function ReferencePanel() {
|
||||
/>
|
||||
<ActionButton
|
||||
icon={
|
||||
node.locked ? (
|
||||
guideLocked ? (
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Unlock className="h-3.5 w-3.5" />
|
||||
)
|
||||
}
|
||||
label={node.locked ? 'Unlock' : 'Lock'}
|
||||
onClick={() => handleUpdate({ locked: node.locked !== true } as Partial<GuideNode>)}
|
||||
label={guideLocked ? 'Unlock' : 'Lock'}
|
||||
onClick={() => setGuideLocked(node.id, !guideLocked)}
|
||||
/>
|
||||
</ActionGroup>
|
||||
|
||||
@@ -224,16 +232,11 @@ export function ReferencePanel() {
|
||||
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
label={node.scaleReference?.visible === false ? 'Show Scale' : 'Hide Scale'}
|
||||
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
||||
disabled={!node.scaleReference}
|
||||
onClick={() => {
|
||||
if (!node.scaleReference) return
|
||||
handleUpdate({
|
||||
scaleReference: {
|
||||
...node.scaleReference,
|
||||
visible: node.scaleReference.visible === false,
|
||||
},
|
||||
} as Partial<GuideNode>)
|
||||
setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
|
||||
@@ -34,8 +34,6 @@ export async function createLocalGuideImage({
|
||||
rotation: [0, 0, 0],
|
||||
scale: 1,
|
||||
opacity: 50,
|
||||
locked: false,
|
||||
showIn3d: false,
|
||||
scaleReference: null,
|
||||
})
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
type LevelNode,
|
||||
type RoofNode,
|
||||
type RoofSegmentNode,
|
||||
type SpawnNode,
|
||||
type RoofSurfaceMaterialRole,
|
||||
type SlabNode,
|
||||
type Space,
|
||||
type SpawnNode,
|
||||
type StairNode,
|
||||
type StairSegmentNode,
|
||||
type StairSurfaceMaterialRole,
|
||||
@@ -115,6 +115,11 @@ type MaterialPaintSelectionSnapshot = {
|
||||
activePaintMaterial: ActivePaintMaterial | null
|
||||
}
|
||||
|
||||
export type GuideUiState = {
|
||||
locked?: boolean
|
||||
scaleReferenceVisible?: boolean
|
||||
}
|
||||
|
||||
type EditorState = {
|
||||
phase: Phase
|
||||
setPhase: (phase: Phase) => void
|
||||
@@ -181,6 +186,10 @@ type EditorState = {
|
||||
setPaintPanelOpen: (open: boolean) => void
|
||||
selectedReferenceId: string | null
|
||||
setSelectedReferenceId: (id: string | null) => void
|
||||
guideUi: Record<string, GuideUiState>
|
||||
setGuideLocked: (guideId: string, locked: boolean) => void
|
||||
setGuideScaleReferenceVisible: (guideId: string, visible: boolean) => void
|
||||
clearGuideUi: (guideId: string) => void
|
||||
// Space detection for cutaway mode
|
||||
spaces: Record<string, Space>
|
||||
setSpaces: (spaces: Record<string, Space>) => void
|
||||
@@ -632,6 +641,36 @@ const useEditor = create<EditorState>()(
|
||||
setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
|
||||
selectedReferenceId: null,
|
||||
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
||||
guideUi: {},
|
||||
setGuideLocked: (guideId, locked) =>
|
||||
set((state) => ({
|
||||
guideUi: {
|
||||
...state.guideUi,
|
||||
[guideId]: {
|
||||
...state.guideUi[guideId],
|
||||
locked,
|
||||
},
|
||||
},
|
||||
})),
|
||||
setGuideScaleReferenceVisible: (guideId, visible) =>
|
||||
set((state) => ({
|
||||
guideUi: {
|
||||
...state.guideUi,
|
||||
[guideId]: {
|
||||
...state.guideUi[guideId],
|
||||
scaleReferenceVisible: visible,
|
||||
},
|
||||
},
|
||||
})),
|
||||
clearGuideUi: (guideId) =>
|
||||
set((state) => {
|
||||
if (!state.guideUi[guideId]) {
|
||||
return state
|
||||
}
|
||||
const guideUi = { ...state.guideUi }
|
||||
delete guideUi[guideId]
|
||||
return { guideUi }
|
||||
}),
|
||||
spaces: {},
|
||||
setSpaces: (spaces) => set({ spaces }),
|
||||
editingHole: null,
|
||||
|
||||
Reference in New Issue
Block a user