Refactor guide UI state and slab rendering

This commit is contained in:
sudhir
2026-05-01 00:33:57 +05:30
parent 1dc9ea0ba8
commit 3df3d12f46
9 changed files with 172 additions and 108 deletions
+2 -1
View File
@@ -33,6 +33,7 @@ export {
} from './hooks/spatial-grid/spatial-grid-sync' } from './hooks/spatial-grid/spatial-grid-sync'
export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query' export { useSpatialQuery } from './hooks/spatial-grid/use-spatial-query'
export { loadAssetUrl, saveAsset } from './lib/asset-storage' export { loadAssetUrl, saveAsset } from './lib/asset-storage'
export { getRenderableSlabPolygon } from './lib/slab-polygon'
export { export {
detectSpacesForLevel, detectSpacesForLevel,
initSpaceDetectionSync, initSpaceDetectionSync,
@@ -71,7 +72,7 @@ export { DoorSystem } from './systems/door/door-system'
export { FenceSystem } from './systems/fence/fence-system' export { FenceSystem } from './systems/fence/fence-system'
export { ItemSystem } from './systems/item/item-system' export { ItemSystem } from './systems/item/item-system'
export { RoofSystem } from './systems/roof/roof-system' export { RoofSystem } from './systems/roof/roof-system'
export { getRenderableSlabPolygon, SlabSystem } from './systems/slab/slab-system' export { SlabSystem } from './systems/slab/slab-system'
export { StairSystem } from './systems/stair/stair-system' export { StairSystem } from './systems/stair/stair-system'
export { export {
getClampedWallCurveOffset, getClampedWallCurveOffset,
+67
View File
@@ -0,0 +1,67 @@
import type { SlabNode } from '../schema'
import { insetPolygonFromCentroid, simplifyClosedPolygon } from './polygon-geometry'
/** Half of default wall thickness — used to extend slab geometry under walls */
const SLAB_OUTSET = 0.05
const AUTO_SLAB_INSET = 0.02
const AUTO_SLAB_SIMPLIFY_TOLERANCE = 0.08
export function getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> {
return slabNode.autoFromWalls
? simplifyClosedPolygon(
insetPolygonFromCentroid(slabNode.polygon, AUTO_SLAB_INSET),
AUTO_SLAB_SIMPLIFY_TOLERANCE,
)
: outsetPolygon(slabNode.polygon, SLAB_OUTSET)
}
/**
* Expand a polygon outward by a uniform distance.
* Offsets each edge outward then intersects consecutive offset edges.
*/
function outsetPolygon(polygon: Array<[number, number]>, amount: number): Array<[number, number]> {
const n = polygon.length
if (n < 3) return polygon
// Determine winding via signed area
let area2 = 0
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
area2 += polygon[i]![0] * polygon[j]![1] - polygon[j]![0] * polygon[i]![1]
}
const s = area2 >= 0 ? 1 : -1
// Offset each edge outward by amount
const offEdges: Array<[number, number, number, number]> = []
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
const dx = polygon[j]![0] - polygon[i]![0]
const dz = polygon[j]![1] - polygon[i]![1]
const len = Math.sqrt(dx * dx + dz * dz)
if (len < 1e-9) {
offEdges.push([polygon[i]![0], polygon[i]![1], dx, dz])
continue
}
const nx = ((s * dz) / len) * amount
const nz = ((s * -dx) / len) * amount
offEdges.push([polygon[i]![0] + nx, polygon[i]![1] + nz, dx, dz])
}
// Intersect consecutive offset edges to get new vertices
const result: Array<[number, number]> = []
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
const [ax, az, adx, adz] = offEdges[i]!
const [bx, bz, bdx, bdz] = offEdges[j]!
const denom = adx * bdz - adz * bdx
if (Math.abs(denom) < 1e-9) {
// Parallel edges — use offset endpoint
result.push([ax + adx, az + adz])
} else {
const t = ((bx - ax) * bdz - (bz - az) * bdx) / denom
result.push([ax + t * adx, az + t * adz])
}
}
return result
}
-3
View File
@@ -9,7 +9,6 @@ export const GuideScaleReference = z.object({
measuredLengthUnits: z.number().positive(), measuredLengthUnits: z.number().positive(),
metersPerUnit: z.number().positive(), metersPerUnit: z.number().positive(),
label: z.string(), label: z.string(),
visible: z.boolean().default(true),
}) })
export const GuideNode = BaseNode.extend({ export const GuideNode = BaseNode.extend({
@@ -20,8 +19,6 @@ export const GuideNode = BaseNode.extend({
rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]), rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
scale: z.number().default(1), scale: z.number().default(1),
opacity: z.number().min(0).max(100).default(50), opacity: z.number().min(0).max(100).default(50),
locked: z.boolean().default(false),
showIn3d: z.boolean().default(false),
scaleReference: GuideScaleReference.nullable().default(null), scaleReference: GuideScaleReference.nullable().default(null),
}) })
+1 -66
View File
@@ -1,7 +1,7 @@
import { useFrame } from '@react-three/fiber' import { useFrame } from '@react-three/fiber'
import * as THREE from 'three' import * as THREE from 'three'
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry' import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
import { insetPolygonFromCentroid, simplifyClosedPolygon } from '../../lib/polygon-geometry' import { getRenderableSlabPolygon } from '../../lib/slab-polygon'
import type { AnyNodeId, SlabNode } from '../../schema' import type { AnyNodeId, SlabNode } from '../../schema'
import useScene from '../../store/use-scene' import useScene from '../../store/use-scene'
@@ -58,71 +58,6 @@ function updateSlabGeometry(node: SlabNode, mesh: THREE.Mesh) {
mesh.position.y = elevation < 0 ? elevation : 0 mesh.position.y = elevation < 0 ? elevation : 0
} }
/** Half of default wall thickness — used to extend slab geometry under walls */
const SLAB_OUTSET = 0.05
const AUTO_SLAB_INSET = 0.02
const AUTO_SLAB_SIMPLIFY_TOLERANCE = 0.08
export function getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> {
return slabNode.autoFromWalls
? simplifyClosedPolygon(
insetPolygonFromCentroid(slabNode.polygon, AUTO_SLAB_INSET),
AUTO_SLAB_SIMPLIFY_TOLERANCE,
)
: outsetPolygon(slabNode.polygon, SLAB_OUTSET)
}
/**
* Expand a polygon outward by a uniform distance.
* Offsets each edge outward then intersects consecutive offset edges.
*/
function outsetPolygon(polygon: Array<[number, number]>, amount: number): Array<[number, number]> {
const n = polygon.length
if (n < 3) return polygon
// Determine winding via signed area
let area2 = 0
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
area2 += polygon[i]![0] * polygon[j]![1] - polygon[j]![0] * polygon[i]![1]
}
const s = area2 >= 0 ? 1 : -1
// Offset each edge outward by amount
const offEdges: Array<[number, number, number, number]> = []
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
const dx = polygon[j]![0] - polygon[i]![0]
const dz = polygon[j]![1] - polygon[i]![1]
const len = Math.sqrt(dx * dx + dz * dz)
if (len < 1e-9) {
offEdges.push([polygon[i]![0], polygon[i]![1], dx, dz])
continue
}
const nx = ((s * dz) / len) * amount
const nz = ((s * -dx) / len) * amount
offEdges.push([polygon[i]![0] + nx, polygon[i]![1] + nz, dx, dz])
}
// Intersect consecutive offset edges to get new vertices
const result: Array<[number, number]> = []
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
const [ax, az, adx, adz] = offEdges[i]!
const [bx, bz, bdx, bdz] = offEdges[j]!
const denom = adx * bdz - adz * bdx
if (Math.abs(denom) < 1e-9) {
// Parallel edges — use offset endpoint
result.push([ax + adx, az + adz])
} else {
const t = ((bx - ax) * bdz - (bz - az) * bdx) / denom
result.push([ax + t * adx, az + t * adz])
}
}
return result
}
/** /**
* Generates extruded slab geometry from polygon * Generates extruded slab geometry from polygon
*/ */
@@ -55,6 +55,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { useShallow } from 'zustand/react/shallow'
import { import {
buildFloorplanItemEntry, buildFloorplanItemEntry,
buildFloorplanStairEntry as buildSharedFloorplanStairEntry, buildFloorplanStairEntry as buildSharedFloorplanStairEntry,
@@ -67,6 +68,7 @@ import { duplicateRoofSubtree } from '../../lib/roof-duplication'
import { sfxEmitter } from '../../lib/sfx-bus' import { sfxEmitter } from '../../lib/sfx-bus'
import { duplicateStairSubtree } from '../../lib/stair-duplication' import { duplicateStairSubtree } from '../../lib/stair-duplication'
import { cn } from '../../lib/utils' import { cn } from '../../lib/utils'
import type { GuideUiState } from '../../store/use-editor'
import useEditor from '../../store/use-editor' import useEditor from '../../store/use-editor'
import { FloorplanActionMenuLayer as Editor2dFloorplanActionMenuLayer } from '../editor-2d/floorplan-action-menu-layer' import { FloorplanActionMenuLayer as Editor2dFloorplanActionMenuLayer } from '../editor-2d/floorplan-action-menu-layer'
import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay' 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({ const FloorplanGuideLayer = memo(function FloorplanGuideLayer({
guideUi,
guides, guides,
isInteractive, isInteractive,
selectedGuideId, selectedGuideId,
@@ -3275,6 +3278,7 @@ const FloorplanGuideLayer = memo(function FloorplanGuideLayer({
onGuideSelect, onGuideSelect,
onGuideTranslateStart, onGuideTranslateStart,
}: { }: {
guideUi: Record<string, GuideUiState>
guides: GuideNode[] guides: GuideNode[]
isInteractive: boolean isInteractive: boolean
selectedGuideId: GuideNode['id'] | null selectedGuideId: GuideNode['id'] | null
@@ -3303,7 +3307,7 @@ const FloorplanGuideLayer = memo(function FloorplanGuideLayer({
activeGuideInteractionGuideId === guide.id ? activeGuideInteractionMode : null activeGuideInteractionGuideId === guide.id ? activeGuideInteractionMode : null
} }
guide={guide} guide={guide}
isInteractive={isInteractive && guide.locked !== true} isInteractive={isInteractive && guideUi[guide.id]?.locked !== true}
isSelected={selectedGuideId === guide.id} isSelected={selectedGuideId === guide.id}
key={guide.id} key={guide.id}
onGuideSelect={onGuideSelect} onGuideSelect={onGuideSelect}
@@ -3405,21 +3409,24 @@ function FloorplanReferenceScaleLine({
function FloorplanReferenceScaleLayer({ function FloorplanReferenceScaleLayer({
draft, draft,
guideUi,
guides, guides,
palette, palette,
unit, unit,
unitsPerPixel, unitsPerPixel,
}: { }: {
draft: ReferenceScaleDraft | null draft: ReferenceScaleDraft | null
guideUi: Record<string, GuideUiState>
guides: GuideNode[] guides: GuideNode[]
palette: FloorplanPalette palette: FloorplanPalette
unit: 'metric' | 'imperial' unit: 'metric' | 'imperial'
unitsPerPixel: number unitsPerPixel: number
}) { }) {
const visibleReferences = guides const visibleReferences = guides
.filter((guide) => guideUi[guide.id]?.scaleReferenceVisible !== false)
.map((guide) => guide.scaleReference) .map((guide) => guide.scaleReference)
.filter((reference): reference is NonNullable<GuideNode['scaleReference']> => .filter((reference): reference is NonNullable<GuideNode['scaleReference']> =>
Boolean(reference && reference.visible !== false), Boolean(reference),
) )
return ( return (
@@ -6170,7 +6177,10 @@ export function FloorplanPanel() {
const showReferenceFloor = useEditor((s) => s.showReferenceFloor) const showReferenceFloor = useEditor((s) => s.showReferenceFloor)
const referenceFloorOffset = useEditor((s) => s.referenceFloorOffset) const referenceFloorOffset = useEditor((s) => s.referenceFloorOffset)
const referenceFloorOpacity = useEditor((s) => s.referenceFloorOpacity) 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>( const [floorplanMarqueeState, setFloorplanMarqueeState] = useState<FloorplanMarqueeState | null>(
null, null,
) )
@@ -6775,24 +6785,33 @@ export function FloorplanPanel() {
return lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null return lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null
}, [floorplanLevels, levelNode, referenceFloorOffset, showReferenceFloor]) }, [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>(() => { const referenceFloorData = useMemo<ReferenceFloorData | null>(() => {
if (!referenceFloorLevel) { if (!referenceFloorLevel) {
return null return null
} }
const children = referenceFloorLevel.children const children = referenceFloorDescendants.filter(
.map((childId) => sceneNodes[childId]) (node) => node.parentId === referenceFloorLevel.id,
.filter((node): node is AnyNode => Boolean(node && node.visible !== false)) )
const referenceWalls = children.filter((node): node is WallNode => node.type === 'wall') const referenceWalls = children.filter((node): node is WallNode => node.type === 'wall')
const referenceFences = children.filter((node): node is FenceNode => node.type === 'fence') const referenceFences = children.filter((node): node is FenceNode => node.type === 'fence')
const referenceSlabs = children.filter((node): node is SlabNode => node.type === 'slab') const referenceSlabs = children.filter((node): node is SlabNode => node.type === 'slab')
const referenceCeilings = children.filter( const referenceCeilings = children.filter(
(node): node is CeilingNode => node.type === 'ceiling', (node): node is CeilingNode => node.type === 'ceiling',
) )
const referenceDescendants = collectLevelDescendants( const referenceDescendants = referenceFloorDescendants
referenceFloorLevel,
sceneNodes as Record<string, AnyNode>,
).filter((node) => node.visible !== false)
const referenceDescendantById = new Map(referenceDescendants.map((node) => [node.id, node])) const referenceDescendantById = new Map(referenceDescendants.map((node) => [node.id, node]))
const referenceFloorplanWalls = referenceWalls.map(getFloorplanWall) const referenceFloorplanWalls = referenceWalls.map(getFloorplanWall)
@@ -6931,7 +6950,7 @@ export function FloorplanPanel() {
slabPolygons, slabPolygons,
wallPolygons, wallPolygons,
} }
}, [referenceFloorLevel, sceneNodes]) }, [referenceFloorDescendants, referenceFloorLevel])
const hasPendingItemMeshFootprints = floorplanItemEntries.some((entry) => !entry.usesRealMesh) const hasPendingItemMeshFootprints = floorplanItemEntries.some((entry) => !entry.usesRealMesh)
const floorplanStairEntries = useMemo( const floorplanStairEntries = useMemo(
() => () =>
@@ -8729,13 +8748,14 @@ export function FloorplanPanel() {
setReferenceScaleDraft((current) => (current?.guideId === payload.guideId ? null : current)) setReferenceScaleDraft((current) => (current?.guideId === payload.guideId ? null : current))
setPendingReferenceScale((current) => (current?.guideId === payload.guideId ? null : current)) setPendingReferenceScale((current) => (current?.guideId === payload.guideId ? null : current))
clearGuideUi(payload.guideId)
} }
emitter.on('guide:deleted', handleDeleted) emitter.on('guide:deleted', handleDeleted)
return () => { return () => {
emitter.off('guide:deleted', handleDeleted) emitter.off('guide:deleted', handleDeleted)
} }
}, []) }, [clearGuideUi])
const handleReferenceScaleConfirm = useCallback(() => { const handleReferenceScaleConfirm = useCallback(() => {
if (!pendingReferenceScale) { if (!pendingReferenceScale) {
@@ -8784,7 +8804,6 @@ export function FloorplanPanel() {
updateNode( updateNode(
pendingReferenceScale.guideId as AnyNodeId, pendingReferenceScale.guideId as AnyNodeId,
{ {
locked: true,
position: nextGuidePosition, position: nextGuidePosition,
scale: nextGuideScale, scale: nextGuideScale,
scaleReference: { scaleReference: {
@@ -8794,10 +8813,11 @@ export function FloorplanPanel() {
measuredLengthUnits: scaledMeasuredLengthUnits, measuredLengthUnits: scaledMeasuredLengthUnits,
metersPerUnit, metersPerUnit,
label: formatReferenceScaleLabel(displayLength, referenceScaleUnit), label: formatReferenceScaleLabel(displayLength, referenceScaleUnit),
visible: true,
}, },
} as Partial<GuideNode>, } as Partial<GuideNode>,
) )
setGuideLocked(pendingReferenceScale.guideId, true)
setGuideScaleReferenceVisible(pendingReferenceScale.guideId, true)
setSelectedReferenceId(pendingReferenceScale.guideId) setSelectedReferenceId(pendingReferenceScale.guideId)
setPendingReferenceScale(null) setPendingReferenceScale(null)
}, [ }, [
@@ -8805,6 +8825,8 @@ export function FloorplanPanel() {
pendingReferenceScale, pendingReferenceScale,
referenceScaleUnit, referenceScaleUnit,
referenceScaleValue, referenceScaleValue,
setGuideLocked,
setGuideScaleReferenceVisible,
setSelectedReferenceId, setSelectedReferenceId,
updateNode, updateNode,
]) ])
@@ -11703,7 +11725,7 @@ export function FloorplanPanel() {
corner: GuideCorner, corner: GuideCorner,
event: ReactPointerEvent<SVGCircleElement>, event: ReactPointerEvent<SVGCircleElement>,
) => { ) => {
if (event.button !== 0 || !canInteractWithGuides || guide.locked === true) { if (event.button !== 0 || !canInteractWithGuides || guideUi[guide.id]?.locked === true) {
return return
} }
@@ -11762,7 +11784,7 @@ export function FloorplanPanel() {
guideTransformDraftRef.current = nextDraft guideTransformDraftRef.current = nextDraft
setGuideTransformDraft(nextDraft) setGuideTransformDraft(nextDraft)
}, },
[canInteractWithGuides, handleGuideSelect, theme], [canInteractWithGuides, guideUi, handleGuideSelect, theme],
) )
const handleGuideTranslateStart = useCallback( const handleGuideTranslateStart = useCallback(
(guide: GuideNode, event: ReactPointerEvent<SVGRectElement>) => { (guide: GuideNode, event: ReactPointerEvent<SVGRectElement>) => {
@@ -11770,7 +11792,7 @@ export function FloorplanPanel() {
event.button !== 0 || event.button !== 0 ||
!canInteractWithGuides || !canInteractWithGuides ||
selectedGuideId !== guide.id || selectedGuideId !== guide.id ||
guide.locked === true guideUi[guide.id]?.locked === true
) { ) {
return return
} }
@@ -11812,7 +11834,7 @@ export function FloorplanPanel() {
guideTransformDraftRef.current = nextDraft guideTransformDraftRef.current = nextDraft
setGuideTransformDraft(nextDraft) setGuideTransformDraft(nextDraft)
}, },
[canInteractWithGuides, getSvgPointFromClientPoint, selectedGuideId], [canInteractWithGuides, getSvgPointFromClientPoint, guideUi, selectedGuideId],
) )
const handleOpeningSelect = useCallback( const handleOpeningSelect = useCallback(
@@ -14183,6 +14205,7 @@ export function FloorplanPanel() {
<FloorplanGuideLayer <FloorplanGuideLayer
activeGuideInteractionGuideId={activeGuideInteractionGuideId} activeGuideInteractionGuideId={activeGuideInteractionGuideId}
activeGuideInteractionMode={activeGuideInteractionMode} activeGuideInteractionMode={activeGuideInteractionMode}
guideUi={guideUi}
guides={displayGuides} guides={displayGuides}
isInteractive={canInteractWithGuides} isInteractive={canInteractWithGuides}
onGuideSelect={handleGuideSelect} onGuideSelect={handleGuideSelect}
@@ -14293,6 +14316,7 @@ export function FloorplanPanel() {
<FloorplanReferenceScaleLayer <FloorplanReferenceScaleLayer
draft={referenceScaleDraft} draft={referenceScaleDraft}
guideUi={guideUi}
guides={displayGuides} guides={displayGuides}
palette={palette} palette={palette}
unit={unit} unit={unit}
@@ -14545,7 +14569,7 @@ export function FloorplanPanel() {
onCornerHoverChange={setHoveredGuideCorner} onCornerHoverChange={setHoveredGuideCorner}
onCornerPointerDown={handleGuideCornerPointerDown} onCornerPointerDown={handleGuideCornerPointerDown}
rotationModifierPressed={rotationModifierPressed} 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 type ReferenceNode = ScanNode | GuideNode
function getScaleStatus(guide: GuideNode) { function getScaleStatus(guide: GuideNode, scaleReferenceVisible: boolean) {
const reference = guide.scaleReference const reference = guide.scaleReference
if (!reference) { if (!reference) {
return 'Uncalibrated' return 'Uncalibrated'
} }
return `${reference.visible === false ? 'Scaled (hidden)' : 'Scaled'} · ${reference.label}` return `${scaleReferenceVisible ? 'Scaled' : 'Scaled (hidden)'} · ${reference.label}`
} }
export function ReferencePanel() { export function ReferencePanel() {
const selectedReferenceId = useEditor((s) => s.selectedReferenceId) const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId) 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 updateNode = useScene((s) => s.updateNode)
const deleteNode = useScene((s) => s.deleteNode) const deleteNode = useScene((s) => s.deleteNode)
const replaceInputRef = useRef<HTMLInputElement>(null) const replaceInputRef = useRef<HTMLInputElement>(null)
@@ -78,13 +82,14 @@ export function ReferencePanel() {
url: assetUrl, url: assetUrl,
scaleReference: null, scaleReference: null,
} as Partial<GuideNode>) } as Partial<GuideNode>)
setGuideScaleReferenceVisible(selectedReferenceId, true)
} catch { } catch {
setReplaceError('Could not replace that image.') setReplaceError('Could not replace that image.')
} finally { } finally {
setIsReplacing(false) setIsReplacing(false)
} }
}, },
[node?.type, selectedReferenceId, updateNode], [node?.type, selectedReferenceId, setGuideScaleReferenceVisible, updateNode],
) )
const handleDeleteGuide = useCallback(() => { const handleDeleteGuide = useCallback(() => {
@@ -94,8 +99,9 @@ export function ReferencePanel() {
deleteNode(selectedReferenceId as AnyNode['id']) deleteNode(selectedReferenceId as AnyNode['id'])
emitter.emit('guide:deleted', { guideId: selectedReferenceId as GuideNode['id'] }) emitter.emit('guide:deleted', { guideId: selectedReferenceId as GuideNode['id'] })
clearGuideUi(selectedReferenceId)
setSelectedReferenceId(null) setSelectedReferenceId(null)
}, [deleteNode, node?.type, selectedReferenceId, setSelectedReferenceId]) }, [clearGuideUi, deleteNode, node?.type, selectedReferenceId, setSelectedReferenceId])
const handleStartScale = useCallback(() => { const handleStartScale = useCallback(() => {
if (node?.type !== 'guide') { if (node?.type !== 'guide') {
@@ -130,7 +136,9 @@ export function ReferencePanel() {
if (!node || (node.type !== 'scan' && node.type !== 'guide')) return null if (!node || (node.type !== 'scan' && node.type !== 'guide')) return null
const isScan = node.type === 'scan' 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 ( return (
<PanelWrapper <PanelWrapper
@@ -184,14 +192,14 @@ export function ReferencePanel() {
/> />
<ActionButton <ActionButton
icon={ icon={
node.locked ? ( guideLocked ? (
<Lock className="h-3.5 w-3.5" /> <Lock className="h-3.5 w-3.5" />
) : ( ) : (
<Unlock className="h-3.5 w-3.5" /> <Unlock className="h-3.5 w-3.5" />
) )
} }
label={node.locked ? 'Unlock' : 'Lock'} label={guideLocked ? 'Unlock' : 'Lock'}
onClick={() => handleUpdate({ locked: node.locked !== true } as Partial<GuideNode>)} onClick={() => setGuideLocked(node.id, !guideLocked)}
/> />
</ActionGroup> </ActionGroup>
@@ -224,16 +232,11 @@ export function ReferencePanel() {
<ActionGroup> <ActionGroup>
<ActionButton <ActionButton
label={node.scaleReference?.visible === false ? 'Show Scale' : 'Hide Scale'} label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
disabled={!node.scaleReference} disabled={!node.scaleReference}
onClick={() => { onClick={() => {
if (!node.scaleReference) return if (!node.scaleReference) return
handleUpdate({ setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)
scaleReference: {
...node.scaleReference,
visible: node.scaleReference.visible === false,
},
} as Partial<GuideNode>)
}} }}
/> />
<ActionButton <ActionButton
@@ -34,8 +34,6 @@ export async function createLocalGuideImage({
rotation: [0, 0, 0], rotation: [0, 0, 0],
scale: 1, scale: 1,
opacity: 50, opacity: 50,
locked: false,
showIn3d: false,
scaleReference: null, scaleReference: null,
}) })
+40 -1
View File
@@ -11,10 +11,10 @@ import {
type LevelNode, type LevelNode,
type RoofNode, type RoofNode,
type RoofSegmentNode, type RoofSegmentNode,
type SpawnNode,
type RoofSurfaceMaterialRole, type RoofSurfaceMaterialRole,
type SlabNode, type SlabNode,
type Space, type Space,
type SpawnNode,
type StairNode, type StairNode,
type StairSegmentNode, type StairSegmentNode,
type StairSurfaceMaterialRole, type StairSurfaceMaterialRole,
@@ -115,6 +115,11 @@ type MaterialPaintSelectionSnapshot = {
activePaintMaterial: ActivePaintMaterial | null activePaintMaterial: ActivePaintMaterial | null
} }
export type GuideUiState = {
locked?: boolean
scaleReferenceVisible?: boolean
}
type EditorState = { type EditorState = {
phase: Phase phase: Phase
setPhase: (phase: Phase) => void setPhase: (phase: Phase) => void
@@ -181,6 +186,10 @@ type EditorState = {
setPaintPanelOpen: (open: boolean) => void setPaintPanelOpen: (open: boolean) => void
selectedReferenceId: string | null selectedReferenceId: string | null
setSelectedReferenceId: (id: string | null) => void 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 // Space detection for cutaway mode
spaces: Record<string, Space> spaces: Record<string, Space>
setSpaces: (spaces: Record<string, Space>) => void setSpaces: (spaces: Record<string, Space>) => void
@@ -632,6 +641,36 @@ const useEditor = create<EditorState>()(
setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }), setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
selectedReferenceId: null, selectedReferenceId: null,
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }), 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: {}, spaces: {},
setSpaces: (spaces) => set({ spaces }), setSpaces: (spaces) => set({ spaces }),
editingHole: null, editingHole: null,
@@ -19,7 +19,7 @@ export const GuideRenderer = ({ node }: { node: GuideNode }) => {
position={node.position} position={node.position}
ref={ref} ref={ref}
rotation={[0, node.rotation[1], 0]} rotation={[0, node.rotation[1], 0]}
visible={showGuides && node.showIn3d === true} visible={showGuides && node.visible !== false}
> >
{resolvedUrl && ( {resolvedUrl && (
<Suspense> <Suspense>