Add lower floor reference overlay
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Icon } from '@iconify/react'
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type BuildingNode,
|
||||
type CeilingNode,
|
||||
@@ -57,6 +58,7 @@ import { createPortal } from 'react-dom'
|
||||
import {
|
||||
buildFloorplanItemEntry,
|
||||
buildFloorplanStairEntry as buildSharedFloorplanStairEntry,
|
||||
collectLevelDescendants,
|
||||
getFloorplanWall as getSharedFloorplanWall,
|
||||
rotatePlanVector as rotateSharedPlanVector,
|
||||
type FloorplanNodeTransform as SharedFloorplanNodeTransform,
|
||||
@@ -582,6 +584,15 @@ type FloorplanItemEntry = {
|
||||
depth: number
|
||||
}
|
||||
|
||||
type ReferenceFloorData = {
|
||||
ceilingPolygons: CeilingPolygonEntry[]
|
||||
fenceEntries: FloorplanFenceEntry[]
|
||||
itemEntries: FloorplanItemEntry[]
|
||||
openingPolygons: OpeningPolygonEntry[]
|
||||
slabPolygons: SlabPolygonEntry[]
|
||||
wallPolygons: WallPolygonEntry[]
|
||||
}
|
||||
|
||||
type FloorplanStairSegmentEntry = {
|
||||
centerLine: FloorplanLineSegment | null
|
||||
innerPoints: string
|
||||
@@ -3611,6 +3622,97 @@ function FloorplanGuideHandleHint({
|
||||
)
|
||||
}
|
||||
|
||||
const FloorplanReferenceFloorLayer = memo(function FloorplanReferenceFloorLayer({
|
||||
data,
|
||||
opacity,
|
||||
}: {
|
||||
data: ReferenceFloorData | null
|
||||
opacity: number
|
||||
}) {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const clampedOpacity = clamp(opacity, 0.1, 0.8)
|
||||
|
||||
return (
|
||||
<g opacity={clampedOpacity} pointerEvents="none">
|
||||
{data.slabPolygons.map(({ path, slab }) => (
|
||||
<path
|
||||
d={path}
|
||||
fill="rgba(100, 116, 139, 0.14)"
|
||||
fillRule="evenodd"
|
||||
key={slab.id}
|
||||
stroke="rgba(100, 116, 139, 0.45)"
|
||||
strokeWidth={1.2}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.ceilingPolygons.map(({ ceiling, path }) => (
|
||||
<path
|
||||
d={path}
|
||||
fill="rgba(245, 158, 11, 0.06)"
|
||||
fillRule="evenodd"
|
||||
key={ceiling.id}
|
||||
stroke="rgba(245, 158, 11, 0.28)"
|
||||
strokeDasharray="6 4"
|
||||
strokeWidth={1}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.wallPolygons.map(({ polygon, points, wall }) =>
|
||||
polygon.length >= 3 ? (
|
||||
<polygon
|
||||
fill="rgba(100, 116, 139, 0.18)"
|
||||
key={wall.id}
|
||||
points={points}
|
||||
stroke="rgba(71, 85, 105, 0.7)"
|
||||
strokeWidth={1.25}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
|
||||
{data.fenceEntries.map(({ fence, path }) => (
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
key={fence.id}
|
||||
stroke="rgba(71, 85, 105, 0.65)"
|
||||
strokeDasharray="5 4"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.5}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.openingPolygons.map(({ opening, points }) => (
|
||||
<polygon
|
||||
fill="rgba(255, 255, 255, 0.72)"
|
||||
key={opening.id}
|
||||
points={points}
|
||||
stroke="rgba(51, 65, 85, 0.72)"
|
||||
strokeWidth={1.1}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
))}
|
||||
|
||||
{data.itemEntries.map(({ item, points }) => (
|
||||
<polygon
|
||||
fill="rgba(71, 85, 105, 0.12)"
|
||||
key={item.id}
|
||||
points={points}
|
||||
stroke="rgba(71, 85, 105, 0.5)"
|
||||
strokeWidth={1}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
})
|
||||
|
||||
const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
||||
canFocusGeometry,
|
||||
canSelectGeometry,
|
||||
@@ -6065,6 +6167,10 @@ export function FloorplanPanel() {
|
||||
const [hoveredGuideCorner, setHoveredGuideCorner] = useState<GuideCorner | null>(null)
|
||||
const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
|
||||
const setFloorplanSelectionTool = useEditor((s) => s.setFloorplanSelectionTool)
|
||||
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 [floorplanMarqueeState, setFloorplanMarqueeState] = useState<FloorplanMarqueeState | null>(
|
||||
null,
|
||||
)
|
||||
@@ -6658,6 +6764,174 @@ export function FloorplanPanel() {
|
||||
]
|
||||
})
|
||||
}, [cursorPoint, floorplanItems, levelDescendantNodeById, movingFloorplanNodeRevision])
|
||||
const referenceFloorLevel = useMemo(() => {
|
||||
if (!(showReferenceFloor && levelNode)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lowerLevels = floorplanLevels
|
||||
.filter((floorLevel) => floorLevel.id !== levelNode.id && floorLevel.level < levelNode.level)
|
||||
.sort((a, b) => b.level - a.level)
|
||||
|
||||
return lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null
|
||||
}, [floorplanLevels, levelNode, referenceFloorOffset, showReferenceFloor])
|
||||
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 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 referenceDescendantById = new Map(referenceDescendants.map((node) => [node.id, node]))
|
||||
|
||||
const referenceFloorplanWalls = referenceWalls.map(getFloorplanWall)
|
||||
const referenceWallMiterData = calculateLevelMiters(referenceFloorplanWalls)
|
||||
const referenceFloorplanWallById = new Map(
|
||||
referenceFloorplanWalls.map((wall) => [wall.id, wall] as const),
|
||||
)
|
||||
|
||||
const wallPolygons = referenceWalls.map((wall) => {
|
||||
const floorplanWall = referenceFloorplanWallById.get(wall.id) ?? getFloorplanWall(wall)
|
||||
const polygon = getWallPlanFootprint(floorplanWall, referenceWallMiterData)
|
||||
|
||||
return {
|
||||
points: formatPolygonPoints(polygon),
|
||||
polygon,
|
||||
wall,
|
||||
}
|
||||
})
|
||||
|
||||
const openingPolygons = referenceDescendants.flatMap((node) => {
|
||||
if (!(node.type === 'door' || node.type === 'window')) {
|
||||
return []
|
||||
}
|
||||
|
||||
const wall = referenceFloorplanWallById.get(node.parentId as WallNode['id'])
|
||||
if (!wall) {
|
||||
return []
|
||||
}
|
||||
|
||||
const polygon = getOpeningFootprint(wall, node)
|
||||
return [
|
||||
{
|
||||
opening: node,
|
||||
points: formatPolygonPoints(polygon),
|
||||
polygon,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const slabPolygons = referenceSlabs.flatMap((slab) => {
|
||||
const polygon = toFloorplanPolygon(slab.polygon)
|
||||
if (polygon.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
||||
const holes = (slab.holes ?? [])
|
||||
.map((hole) => toFloorplanPolygon(hole))
|
||||
.filter((hole) => hole.length >= 3)
|
||||
const visualPolygon = toFloorplanPolygon(getRenderableSlabPolygon(slab))
|
||||
const visualHoles = holes
|
||||
|
||||
return [
|
||||
{
|
||||
slab,
|
||||
polygon,
|
||||
holes,
|
||||
visualPolygon,
|
||||
visualHoles,
|
||||
path: formatPolygonPath(visualPolygon, visualHoles),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const ceilingPolygons = referenceCeilings.flatMap((ceiling) => {
|
||||
const polygon = toFloorplanPolygon(ceiling.polygon)
|
||||
if (polygon.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
||||
const holes = (ceiling.holes ?? [])
|
||||
.map((hole) => toFloorplanPolygon(hole))
|
||||
.filter((hole) => hole.length >= 3)
|
||||
|
||||
return [
|
||||
{
|
||||
ceiling,
|
||||
polygon,
|
||||
holes,
|
||||
path: formatPolygonPath(polygon, holes),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const fenceEntries = referenceFences.flatMap((fence) => {
|
||||
const centerline = isCurvedWall(fence)
|
||||
? sampleWallCenterline(fence, 24)
|
||||
: [
|
||||
{ x: fence.start[0], y: fence.start[1] },
|
||||
{ x: fence.end[0], y: fence.end[1] },
|
||||
]
|
||||
const path = buildSvgPolylinePath(centerline)
|
||||
if (!path) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{ fence, centerline, markerFrames: [], path }]
|
||||
})
|
||||
|
||||
const transformCache = new Map<string, SharedFloorplanNodeTransform | null>()
|
||||
const itemEntries = referenceDescendants.flatMap((node) => {
|
||||
if (
|
||||
!(
|
||||
node.type === 'item' &&
|
||||
node.asset.category !== 'door' &&
|
||||
node.asset.category !== 'window'
|
||||
)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const entry = buildFloorplanItemEntry(node, referenceDescendantById, transformCache)
|
||||
if (!entry) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
dimensionPolygon: entry.dimensionPolygon,
|
||||
item: entry.item,
|
||||
points: formatPolygonPoints(entry.polygon),
|
||||
polygon: entry.polygon,
|
||||
usesRealMesh: entry.usesRealMesh,
|
||||
center: entry.center,
|
||||
rotation: entry.rotation,
|
||||
width: entry.width,
|
||||
depth: entry.depth,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
ceilingPolygons,
|
||||
fenceEntries,
|
||||
itemEntries,
|
||||
openingPolygons,
|
||||
slabPolygons,
|
||||
wallPolygons,
|
||||
}
|
||||
}, [referenceFloorLevel, sceneNodes])
|
||||
const hasPendingItemMeshFootprints = floorplanItemEntries.some((entry) => !entry.usesRealMesh)
|
||||
const floorplanStairEntries = useMemo(
|
||||
() =>
|
||||
@@ -13901,6 +14175,11 @@ export function FloorplanPanel() {
|
||||
showGrid={showGrid}
|
||||
/>
|
||||
|
||||
<FloorplanReferenceFloorLayer
|
||||
data={referenceFloorData}
|
||||
opacity={referenceFloorOpacity}
|
||||
/>
|
||||
|
||||
<FloorplanGuideLayer
|
||||
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
||||
activeGuideInteractionMode={activeGuideInteractionMode}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type BuildingNode,
|
||||
type GuideNode,
|
||||
type LevelNode,
|
||||
type ScanNode,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { Check, ChevronDown, Plus, Trash2 } from 'lucide-react'
|
||||
import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { createLocalGuideImage } from '../../../lib/local-guide-image'
|
||||
@@ -60,6 +61,28 @@ function useLevelScans(): ScanNode[] {
|
||||
)
|
||||
}
|
||||
|
||||
function useLowerReferenceLevels(): LevelNode[] {
|
||||
const levelId = useViewer((s) => s.selection.levelId)
|
||||
return useScene(
|
||||
useShallow((state) => {
|
||||
if (!levelId) return [] as LevelNode[]
|
||||
const activeLevel = state.nodes[levelId]
|
||||
if (!activeLevel || activeLevel.type !== 'level') return [] as LevelNode[]
|
||||
const buildingId = activeLevel.parentId as BuildingNode['id'] | undefined
|
||||
const building = buildingId ? state.nodes[buildingId] : null
|
||||
if (!building || building.type !== 'building') return [] as LevelNode[]
|
||||
|
||||
return (building.children ?? [])
|
||||
.map((id) => state.nodes[id])
|
||||
.filter(
|
||||
(node): node is LevelNode =>
|
||||
node?.type === 'level' && node.id !== levelId && node.level < activeLevel.level,
|
||||
)
|
||||
.sort((a, b) => b.level - a.level)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Shared upload button for dropdowns ──────────────────────────────────────
|
||||
|
||||
function UploadButton({ onError }: { onError: (message: string | null) => void }) {
|
||||
@@ -578,6 +601,156 @@ function ScansControl() {
|
||||
)
|
||||
}
|
||||
|
||||
function ReferenceFloorControl() {
|
||||
const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
|
||||
const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
|
||||
const referenceFloorOffset = useEditor((state) => state.referenceFloorOffset)
|
||||
const setReferenceFloorOffset = useEditor((state) => state.setReferenceFloorOffset)
|
||||
const referenceFloorOpacity = useEditor((state) => state.referenceFloorOpacity)
|
||||
const setReferenceFloorOpacity = useEditor((state) => state.setReferenceFloorOpacity)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const lowerLevels = useLowerReferenceLevels()
|
||||
const hasLowerLevels = lowerLevels.length > 0
|
||||
const selectedLevel = lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={setIsOpen} open={isOpen}>
|
||||
<div className="flex items-center">
|
||||
<ActionButton
|
||||
className={cn(
|
||||
'rounded-r-none p-0',
|
||||
showReferenceFloor && selectedLevel
|
||||
? 'bg-white/15'
|
||||
: 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
|
||||
)}
|
||||
disabled={!hasLowerLevels}
|
||||
label={
|
||||
selectedLevel && showReferenceFloor
|
||||
? `Reference floor: ${selectedLevel.name}`
|
||||
: 'Reference floor'
|
||||
}
|
||||
onClick={() => {
|
||||
if (hasLowerLevels) toggleReferenceFloor()
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<div className="relative">
|
||||
<Layers2 className="h-4 w-4" />
|
||||
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
|
||||
{lowerLevels.length}
|
||||
</span>
|
||||
</div>
|
||||
</ActionButton>
|
||||
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
aria-expanded={isOpen}
|
||||
aria-label="Reference floor settings"
|
||||
className={cn(
|
||||
'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
|
||||
showReferenceFloor && selectedLevel
|
||||
? isOpen
|
||||
? 'bg-white/10'
|
||||
: 'bg-white/5 hover:bg-white/8'
|
||||
: isOpen
|
||||
? 'bg-white/8'
|
||||
: 'opacity-60 hover:bg-white/5 hover:opacity-100',
|
||||
)}
|
||||
disabled={!hasLowerLevels}
|
||||
type="button"
|
||||
>
|
||||
<ChevronDown className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
|
||||
<PopoverContent
|
||||
align="center"
|
||||
className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-[0_14px_28px_-18px_rgba(15,23,42,0.55),0_6px_16px_-10px_rgba(15,23,42,0.2)] backdrop-blur-xl"
|
||||
side="top"
|
||||
sideOffset={14}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
|
||||
<Layers2 className="h-4 w-4" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-foreground text-sm">Reference floor</p>
|
||||
{selectedLevel && (
|
||||
<p className="truncate text-muted-foreground text-xs">{selectedLevel.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
aria-label={showReferenceFloor ? 'Hide reference floor' : 'Show reference floor'}
|
||||
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
|
||||
disabled={!hasLowerLevels}
|
||||
onClick={toggleReferenceFloor}
|
||||
type="button"
|
||||
>
|
||||
{showReferenceFloor ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{hasLowerLevels ? (
|
||||
<>
|
||||
<div className="max-h-44 space-y-1 overflow-y-auto rounded-xl border border-border/45 bg-background/60 p-1.5">
|
||||
{lowerLevels.map((level, index) => {
|
||||
const isSelected = referenceFloorOffset === index + 1
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-white/8',
|
||||
isSelected && showReferenceFloor && 'bg-white/10 text-foreground',
|
||||
)}
|
||||
key={level.id}
|
||||
onClick={() => {
|
||||
setReferenceFloorOffset(index + 1)
|
||||
if (!showReferenceFloor) {
|
||||
toggleReferenceFloor()
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 rounded-full border',
|
||||
isSelected && showReferenceFloor
|
||||
? 'border-foreground bg-foreground'
|
||||
: 'border-muted-foreground/35',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate">{level.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{index + 1} below
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<SliderControl
|
||||
label="Opacity"
|
||||
max={0.8}
|
||||
min={0.1}
|
||||
onChange={setReferenceFloorOpacity}
|
||||
precision={2}
|
||||
step={0.05}
|
||||
value={referenceFloorOpacity}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
|
||||
No lower floor available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main ViewToggles ────────────────────────────────────────────────────────
|
||||
|
||||
export function ViewToggles() {
|
||||
@@ -588,6 +761,8 @@ export function ViewToggles() {
|
||||
|
||||
{/* Guides (toggle + dropdown) */}
|
||||
<GuidesControl />
|
||||
|
||||
<ReferenceFloorControl />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -599,6 +774,7 @@ export function SecondaryToggles() {
|
||||
<GridSnapControl />
|
||||
<ScansControl />
|
||||
<GuidesControl />
|
||||
<ReferenceFloorControl />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,11 @@ export type MovingFenceEndpoint = {
|
||||
endpoint: 'start' | 'end'
|
||||
}
|
||||
|
||||
export type MaterialTargetRole = WallSurfaceSide | StairSurfaceMaterialRole | RoofSurfaceMaterialRole | SingleSurfaceMaterialRole
|
||||
export type MaterialTargetRole =
|
||||
| WallSurfaceSide
|
||||
| StairSurfaceMaterialRole
|
||||
| RoofSurfaceMaterialRole
|
||||
| SingleSurfaceMaterialRole
|
||||
|
||||
export type SelectedMaterialTarget = {
|
||||
nodeId: AnyNodeId
|
||||
@@ -197,6 +201,13 @@ type EditorState = {
|
||||
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
||||
gridSnapStep: GridSnapStep
|
||||
setGridSnapStep: (step: GridSnapStep) => void
|
||||
showReferenceFloor: boolean
|
||||
toggleReferenceFloor: () => void
|
||||
setShowReferenceFloor: (show: boolean) => void
|
||||
referenceFloorOffset: number
|
||||
setReferenceFloorOffset: (offset: number) => void
|
||||
referenceFloorOpacity: number
|
||||
setReferenceFloorOpacity: (opacity: number) => void
|
||||
// First-person walkthrough mode (street view)
|
||||
isFirstPersonMode: boolean
|
||||
_viewModeBeforeFirstPerson: ViewMode | null
|
||||
@@ -226,6 +237,9 @@ type PersistedEditorLayoutState = Pick<
|
||||
| 'splitOrientation'
|
||||
| 'floorplanSelectionTool'
|
||||
| 'gridSnapStep'
|
||||
| 'showReferenceFloor'
|
||||
| 'referenceFloorOffset'
|
||||
| 'referenceFloorOpacity'
|
||||
>
|
||||
type PersistedEditorState = PersistedEditorUiState & PersistedEditorLayoutState
|
||||
|
||||
@@ -245,6 +259,9 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
||||
splitOrientation: 'horizontal',
|
||||
floorplanSelectionTool: 'click',
|
||||
gridSnapStep: 0.5,
|
||||
showReferenceFloor: false,
|
||||
referenceFloorOffset: 1,
|
||||
referenceFloorOpacity: 0.35,
|
||||
}
|
||||
|
||||
const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
||||
@@ -354,6 +371,16 @@ function normalizePersistedEditorLayoutState(
|
||||
gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
|
||||
? (state?.gridSnapStep as GridSnapStep)
|
||||
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
||||
showReferenceFloor: state?.showReferenceFloor === true,
|
||||
referenceFloorOffset:
|
||||
typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
|
||||
? Math.floor(state.referenceFloorOffset)
|
||||
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOffset,
|
||||
referenceFloorOpacity:
|
||||
typeof state?.referenceFloorOpacity === 'number' &&
|
||||
Number.isFinite(state.referenceFloorOpacity)
|
||||
? Math.min(0.8, Math.max(0.1, state.referenceFloorOpacity))
|
||||
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +659,16 @@ const useEditor = create<EditorState>()(
|
||||
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
||||
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
||||
setGridSnapStep: (step) => set({ gridSnapStep: step }),
|
||||
showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
|
||||
toggleReferenceFloor: () =>
|
||||
set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
|
||||
setShowReferenceFloor: (show) => set({ showReferenceFloor: show }),
|
||||
referenceFloorOffset: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOffset,
|
||||
setReferenceFloorOffset: (offset) =>
|
||||
set({ referenceFloorOffset: Math.max(1, Math.floor(offset)) }),
|
||||
referenceFloorOpacity: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.referenceFloorOpacity,
|
||||
setReferenceFloorOpacity: (opacity) =>
|
||||
set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
|
||||
allowUndergroundCamera: false,
|
||||
setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
|
||||
isFirstPersonMode: false,
|
||||
@@ -702,6 +739,9 @@ const useEditor = create<EditorState>()(
|
||||
splitOrientation: state.splitOrientation,
|
||||
floorplanSelectionTool: state.floorplanSelectionTool,
|
||||
gridSnapStep: state.gridSnapStep,
|
||||
showReferenceFloor: state.showReferenceFloor,
|
||||
referenceFloorOffset: state.referenceFloorOffset,
|
||||
referenceFloorOpacity: state.referenceFloorOpacity,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user