Refactor guide UI state and slab rendering
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user