Merge pull request #128 from pascalorg/fix/remove-is-editor
Fix/remove is editor
This commit is contained in:
@@ -1,6 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNode, type AnyNodeId, ItemNode, WindowNode, DoorNode, sceneRegistry, useScene } from '@pascal-app/core'
|
import {
|
||||||
|
type AnyNode,
|
||||||
|
type AnyNodeId,
|
||||||
|
DoorNode,
|
||||||
|
ItemNode,
|
||||||
|
sceneRegistry,
|
||||||
|
useScene,
|
||||||
|
WindowNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { Html } from '@react-three/drei'
|
import { Html } from '@react-three/drei'
|
||||||
import { useFrame } from '@react-three/fiber'
|
import { useFrame } from '@react-three/fiber'
|
||||||
@@ -18,7 +26,6 @@ export function FloatingActionMenu() {
|
|||||||
const deleteNode = useScene((s) => s.deleteNode)
|
const deleteNode = useScene((s) => s.deleteNode)
|
||||||
const setMovingNode = useEditor((s) => s.setMovingNode)
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
||||||
const setSelection = useViewer((s) => s.setSelection)
|
const setSelection = useViewer((s) => s.setSelection)
|
||||||
const isEditor = useViewer((state) => state.isEditor)
|
|
||||||
|
|
||||||
const groupRef = useRef<THREE.Group>(null)
|
const groupRef = useRef<THREE.Group>(null)
|
||||||
|
|
||||||
@@ -42,61 +49,70 @@ export function FloatingActionMenu() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleMove = useCallback((e: React.MouseEvent) => {
|
const handleMove = useCallback(
|
||||||
e.stopPropagation()
|
(e: React.MouseEvent) => {
|
||||||
if (!node) return
|
e.stopPropagation()
|
||||||
sfxEmitter.emit('sfx:item-pick')
|
if (!node) return
|
||||||
if (node.type === 'item' || node.type === 'window' || node.type === 'door') {
|
sfxEmitter.emit('sfx:item-pick')
|
||||||
setMovingNode(node as any)
|
if (node.type === 'item' || node.type === 'window' || node.type === 'door') {
|
||||||
}
|
setMovingNode(node as any)
|
||||||
setSelection({ selectedIds: [] })
|
|
||||||
}, [node, setMovingNode, setSelection])
|
|
||||||
|
|
||||||
const handleDuplicate = useCallback((e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (!node || !node.parentId) return
|
|
||||||
sfxEmitter.emit('sfx:item-pick')
|
|
||||||
useScene.temporal.getState().pause()
|
|
||||||
|
|
||||||
let duplicateInfo = structuredClone(node) as any
|
|
||||||
delete duplicateInfo.id
|
|
||||||
duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
|
|
||||||
|
|
||||||
let duplicate: AnyNode | null = null
|
|
||||||
try {
|
|
||||||
if (node.type === 'door') {
|
|
||||||
duplicate = DoorNode.parse(duplicateInfo)
|
|
||||||
} else if (node.type === 'window') {
|
|
||||||
duplicate = WindowNode.parse(duplicateInfo)
|
|
||||||
} else if (node.type === 'item') {
|
|
||||||
duplicate = ItemNode.parse(duplicateInfo)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse duplicate', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duplicate) {
|
|
||||||
if (duplicate.type === 'door' || duplicate.type === 'window') {
|
|
||||||
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
||||||
}
|
|
||||||
if (duplicate.type === 'item' || duplicate.type === 'window' || duplicate.type === 'door') {
|
|
||||||
setMovingNode(duplicate as any)
|
|
||||||
}
|
}
|
||||||
setSelection({ selectedIds: [] })
|
setSelection({ selectedIds: [] })
|
||||||
}
|
},
|
||||||
}, [node, setMovingNode, setSelection])
|
[node, setMovingNode, setSelection],
|
||||||
|
)
|
||||||
|
|
||||||
const handleDelete = useCallback((e: React.MouseEvent) => {
|
const handleDuplicate = useCallback(
|
||||||
e.stopPropagation()
|
(e: React.MouseEvent) => {
|
||||||
if (!selectedId || !node) return
|
e.stopPropagation()
|
||||||
sfxEmitter.emit('sfx:item-delete')
|
if (!node || !node.parentId) return
|
||||||
deleteNode(selectedId as AnyNodeId)
|
sfxEmitter.emit('sfx:item-pick')
|
||||||
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
useScene.temporal.getState().pause()
|
||||||
setSelection({ selectedIds: [] })
|
|
||||||
}, [selectedId, node, deleteNode, setSelection])
|
|
||||||
|
|
||||||
if (!isEditor || !selectedId || !node || !isValidType) return null
|
let duplicateInfo = structuredClone(node) as any
|
||||||
|
delete duplicateInfo.id
|
||||||
|
duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
|
||||||
|
|
||||||
|
let duplicate: AnyNode | null = null
|
||||||
|
try {
|
||||||
|
if (node.type === 'door') {
|
||||||
|
duplicate = DoorNode.parse(duplicateInfo)
|
||||||
|
} else if (node.type === 'window') {
|
||||||
|
duplicate = WindowNode.parse(duplicateInfo)
|
||||||
|
} else if (node.type === 'item') {
|
||||||
|
duplicate = ItemNode.parse(duplicateInfo)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse duplicate', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicate) {
|
||||||
|
if (duplicate.type === 'door' || duplicate.type === 'window') {
|
||||||
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
||||||
|
}
|
||||||
|
if (duplicate.type === 'item' || duplicate.type === 'window' || duplicate.type === 'door') {
|
||||||
|
setMovingNode(duplicate as any)
|
||||||
|
}
|
||||||
|
setSelection({ selectedIds: [] })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[node, setMovingNode, setSelection],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (!selectedId || !node) return
|
||||||
|
sfxEmitter.emit('sfx:item-delete')
|
||||||
|
deleteNode(selectedId as AnyNodeId)
|
||||||
|
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
||||||
|
setSelection({ selectedIds: [] })
|
||||||
|
},
|
||||||
|
[selectedId, node, deleteNode, setSelection],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!selectedId || !node || !isValidType) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
@@ -105,10 +121,10 @@ export function FloatingActionMenu() {
|
|||||||
zIndexRange={[100, 0]}
|
zIndexRange={[100, 0]}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
touchAction: 'none'
|
touchAction: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 p-1 rounded-lg border border-border bg-background/95 shadow-xl backdrop-blur-md"
|
className="flex items-center gap-1 p-1 rounded-lg border border-border bg-background/95 shadow-xl backdrop-blur-md"
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onPointerUp={(e) => e.stopPropagation()}
|
onPointerUp={(e) => e.stopPropagation()}
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import { ExportManager } from './export-manager'
|
|||||||
import { FloatingActionMenu } from './floating-action-menu'
|
import { FloatingActionMenu } from './floating-action-menu'
|
||||||
import { Grid } from './grid'
|
import { Grid } from './grid'
|
||||||
import { SelectionManager } from './selection-manager'
|
import { SelectionManager } from './selection-manager'
|
||||||
|
import { SiteEdgeLabels } from './site-edge-labels'
|
||||||
import { ThumbnailGenerator } from './thumbnail-generator'
|
import { ThumbnailGenerator } from './thumbnail-generator'
|
||||||
|
|
||||||
|
|
||||||
// Load default scene initially (will be replaced when project loads)
|
// Load default scene initially (will be replaced when project loads)
|
||||||
useScene.getState().loadScene()
|
useScene.getState().loadScene()
|
||||||
initSpatialGridSync()
|
initSpatialGridSync()
|
||||||
@@ -106,7 +108,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
<SidebarProvider className="fixed z-20">
|
<SidebarProvider className="fixed z-20">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
<Viewer selectionManager="custom" isEditor={true}>
|
<Viewer selectionManager="custom">
|
||||||
<SelectionManager />
|
<SelectionManager />
|
||||||
<FloatingActionMenu />
|
<FloatingActionMenu />
|
||||||
<ExportManager />
|
<ExportManager />
|
||||||
@@ -118,6 +120,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
<ToolManager />
|
<ToolManager />
|
||||||
<CustomCameraControls />
|
<CustomCameraControls />
|
||||||
<ThumbnailGenerator projectId={projectId} />
|
<ThumbnailGenerator projectId={projectId} />
|
||||||
|
<SiteEdgeLabels />
|
||||||
</Viewer>
|
</Viewer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { sceneRegistry, useScene } from '@pascal-app/core'
|
||||||
|
import type { SiteNode } from '@pascal-app/core'
|
||||||
|
import { Html } from '@react-three/drei'
|
||||||
|
import { createPortal, useFrame } from '@react-three/fiber'
|
||||||
|
import { useMemo, useRef, useState } from 'react'
|
||||||
|
import type { Object3D } from 'three'
|
||||||
|
|
||||||
|
export function SiteEdgeLabels() {
|
||||||
|
const rootNodeIds = useScene((state) => state.rootNodeIds)
|
||||||
|
const nodes = useScene((state) => state.nodes)
|
||||||
|
|
||||||
|
const siteNode = rootNodeIds[0] ? (nodes[rootNodeIds[0]] as SiteNode) : null
|
||||||
|
const siteNodeId = siteNode?.id
|
||||||
|
|
||||||
|
const [siteObj, setSiteObj] = useState<Object3D | null>(null)
|
||||||
|
const prevSiteNodeIdRef = useRef<string | undefined>(undefined)
|
||||||
|
|
||||||
|
// Poll each frame until the site group is registered.
|
||||||
|
// Also resets when the site node ID changes (new project loaded).
|
||||||
|
useFrame(() => {
|
||||||
|
if (siteNodeId !== prevSiteNodeIdRef.current) {
|
||||||
|
prevSiteNodeIdRef.current = siteNodeId
|
||||||
|
setSiteObj(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (siteObj || !siteNodeId) return
|
||||||
|
const obj = sceneRegistry.nodes.get(siteNodeId)
|
||||||
|
if (obj) setSiteObj(obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
const edges = useMemo(() => {
|
||||||
|
const polygon = siteNode?.polygon?.points ?? []
|
||||||
|
if (polygon.length < 2) return []
|
||||||
|
return polygon.map(([x1, z1], i) => {
|
||||||
|
const [x2, z2] = polygon[(i + 1) % polygon.length]!
|
||||||
|
const midX = (x1! + x2) / 2
|
||||||
|
const midZ = (z1! + z2) / 2
|
||||||
|
const dist = Math.sqrt((x2 - x1!) ** 2 + (z2 - z1!) ** 2)
|
||||||
|
return { midX, midZ, dist }
|
||||||
|
})
|
||||||
|
}, [siteNode?.polygon?.points])
|
||||||
|
|
||||||
|
if (!siteObj || edges.length === 0) return null
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<>
|
||||||
|
{edges.map((edge, i) => (
|
||||||
|
<Html
|
||||||
|
center
|
||||||
|
key={`edge-${i}`}
|
||||||
|
position={[edge.midX, 0.5, edge.midZ]}
|
||||||
|
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||||
|
zIndexRange={[10, 0]}
|
||||||
|
occlude
|
||||||
|
>
|
||||||
|
<div className="whitespace-nowrap rounded bg-black/75 px-1.5 py-0.5 font-mono text-white text-xs backdrop-blur-sm">
|
||||||
|
{edge.dist.toFixed(2)}m
|
||||||
|
</div>
|
||||||
|
</Html>
|
||||||
|
))}
|
||||||
|
</>,
|
||||||
|
siteObj,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -958,7 +958,7 @@ function ZoneItem({ zone, isLast }: { zone: ZoneNode, isLast?: boolean }) {
|
|||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 size-3 shrink-0 rounded-sm border border-border/50 transition-all hover:scale-110 cursor-pointer",
|
"mr-2 size-3 shrink-0 rounded-sm border border-border/50 transition-all hover:scale-110 cursor-pointer",
|
||||||
!isSelected && "opacity-60 grayscale"
|
!isSelected && "opacity-40"
|
||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
style={{ backgroundColor: zone.color }}
|
style={{ backgroundColor: zone.color }}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { type SiteNode, useRegistry } from '@pascal-app/core'
|
import { type SiteNode, useRegistry } from '@pascal-app/core'
|
||||||
import { Html } from '@react-three/drei'
|
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { BufferGeometry, Float32BufferAttribute, type Group, Shape } from 'three'
|
import { BufferGeometry, Float32BufferAttribute, type Group, Shape } from 'three'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
import useViewer from '../../../store/use-viewer'
|
|
||||||
import { NodeRenderer } from '../node-renderer'
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
const Y_OFFSET = 0.01
|
const Y_OFFSET = 0.01
|
||||||
const LINE_HEIGHT = 0.5
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates simple line geometry for site boundary
|
* Creates simple line geometry for site boundary
|
||||||
@@ -62,22 +59,6 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
|
|||||||
return createBoundaryLineGeometry(node.polygon.points)
|
return createBoundaryLineGeometry(node.polygon.points)
|
||||||
}, [node?.polygon?.points])
|
}, [node?.polygon?.points])
|
||||||
|
|
||||||
const isEditor = useViewer((state) => state.isEditor)
|
|
||||||
|
|
||||||
// Edge distances for labels
|
|
||||||
const edges = useMemo(() => {
|
|
||||||
if (!isEditor) return []
|
|
||||||
const polygon = node?.polygon?.points ?? []
|
|
||||||
if (polygon.length < 2) return []
|
|
||||||
return polygon.map(([x1, z1], i) => {
|
|
||||||
const [x2, z2] = polygon[(i + 1) % polygon.length]!
|
|
||||||
const midX = (x1! + x2) / 2
|
|
||||||
const midZ = (z1! + z2) / 2
|
|
||||||
const dist = Math.sqrt((x2 - x1!) ** 2 + (z2 - z1!) ** 2)
|
|
||||||
return { midX, midZ, dist }
|
|
||||||
})
|
|
||||||
}, [node?.polygon?.points, isEditor])
|
|
||||||
|
|
||||||
const handlers = useNodeEvents(node, 'site')
|
const handlers = useNodeEvents(node, 'site')
|
||||||
|
|
||||||
if (!node || !floorShape || !lineGeometry) {
|
if (!node || !floorShape || !lineGeometry) {
|
||||||
@@ -106,21 +87,6 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
|
|||||||
<lineBasicMaterial color="#f59e0b" linewidth={2} transparent opacity={0.6} />
|
<lineBasicMaterial color="#f59e0b" linewidth={2} transparent opacity={0.6} />
|
||||||
</line>
|
</line>
|
||||||
|
|
||||||
{/* Edge distance labels */}
|
|
||||||
{isEditor && edges.map((edge, i) => (
|
|
||||||
<Html
|
|
||||||
center
|
|
||||||
key={`edge-${i}`}
|
|
||||||
position={[edge.midX, 0.5, edge.midZ]}
|
|
||||||
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
||||||
zIndexRange={[10, 0]}
|
|
||||||
occlude
|
|
||||||
>
|
|
||||||
<div className="whitespace-nowrap rounded bg-black/75 px-1.5 py-0.5 font-mono text-white text-xs backdrop-blur-sm">
|
|
||||||
{edge.dist.toFixed(2)}m
|
|
||||||
</div>
|
|
||||||
</Html>
|
|
||||||
))}
|
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useRegistry, type ZoneNode, useScene } from '@pascal-app/core'
|
import { useRegistry, type ZoneNode } from '@pascal-app/core'
|
||||||
import { Html } from '@react-three/drei'
|
import { Html } from '@react-three/drei'
|
||||||
import { useMemo, useRef, useState, useEffect } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
||||||
import { color, float, uniform, uv } from 'three/tsl'
|
import { color, float, uniform, uv } from 'three/tsl'
|
||||||
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
import useViewer from '../../../store/use-viewer'
|
|
||||||
|
|
||||||
const Y_OFFSET = 0.01
|
const Y_OFFSET = 0.01
|
||||||
const WALL_HEIGHT = 2.3
|
const WALL_HEIGHT = 2.3
|
||||||
@@ -105,43 +104,6 @@ const createWallGeometry = (polygon: Array<[number, number]>): BufferGeometry =>
|
|||||||
|
|
||||||
export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
||||||
const ref = useRef<Group>(null!)
|
const ref = useRef<Group>(null!)
|
||||||
const updateNode = useScene((s) => s.updateNode)
|
|
||||||
const isEditor = useViewer((state) => state.isEditor)
|
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
|
||||||
const [editValue, setEditValue] = useState(node.name || '')
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing) {
|
|
||||||
setEditValue(node.name || '')
|
|
||||||
setTimeout(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
inputRef.current.select()
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}, [isEditing, node.name])
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
const trimmed = editValue.trim()
|
|
||||||
if (trimmed !== node.name) {
|
|
||||||
updateNode(node.id, { name: trimmed || 'Zone' })
|
|
||||||
}
|
|
||||||
setIsEditing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSave()
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsEditing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useRegistry(node.id, 'zone', ref)
|
useRegistry(node.id, 'zone', ref)
|
||||||
|
|
||||||
@@ -215,71 +177,20 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={ref} {...handlers} userData={{
|
<group ref={ref} {...handlers} userData={{ labelPosition: [centroid[0], 1, centroid[1]] }}>
|
||||||
labelPosition: [centroid[0], 1, centroid[1]]
|
<Html
|
||||||
}}>
|
name="label"
|
||||||
<Html name="label" position={[centroid[0], 1, centroid[1]]} style={{
|
position={[centroid[0], 1, centroid[1]]}
|
||||||
pointerEvents: isEditor ? 'auto' : 'none'
|
style={{ pointerEvents: 'none' }}
|
||||||
}}
|
zIndexRange={[10, 0]}
|
||||||
zIndexRange={[10, 0]}>
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
transform: 'translate3d(-50%, -50%, 0)',
|
transform: 'translate3d(-50%, -50%, 0)',
|
||||||
width: 'max-content',
|
width: 'max-content',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
||||||
display: 'flex',
|
|
||||||
gap: '8px',
|
|
||||||
alignItems: 'center',
|
|
||||||
cursor: isEditor && !isEditing ? 'text' : 'default',
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => isEditor && setIsHovered(true)}
|
|
||||||
onMouseLeave={() => isEditor && setIsHovered(false)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (isEditor && !isEditing) {
|
|
||||||
e.stopPropagation()
|
|
||||||
setIsEditing(true)
|
|
||||||
}
|
|
||||||
}}>
|
}}>
|
||||||
{isEditing ? (
|
<span>{node.name}</span>
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={editValue}
|
|
||||||
onChange={(e) => setEditValue(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onBlur={handleSave}
|
|
||||||
placeholder="Zone"
|
|
||||||
style={{
|
|
||||||
background: 'transparent',
|
|
||||||
color: 'white',
|
|
||||||
textShadow: 'inherit',
|
|
||||||
border: 'none',
|
|
||||||
borderBottom: '1px solid white',
|
|
||||||
outline: 'none',
|
|
||||||
padding: 0,
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 'inherit',
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
width: `${Math.max(editValue.length, 4) + 1}ch`,
|
|
||||||
minWidth: '50px',
|
|
||||||
textAlign: 'center'
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
onDoubleClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>{node.name}</span>
|
|
||||||
{isEditor && (
|
|
||||||
<div style={{ opacity: isHovered ? 1 : 0, transition: 'opacity 0.2s', display: 'flex', alignItems: 'center' }}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
|
||||||
<path d="m15 5 4 4"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
{/* Floor fill */}
|
{/* Floor fill */}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { Bvh } from '@react-three/drei'
|
import { Bvh } from '@react-three/drei'
|
||||||
import { Canvas, extend, type ThreeToJSXElements, useFrame } from '@react-three/fiber'
|
import { Canvas, extend, type ThreeToJSXElements, useFrame } from '@react-three/fiber'
|
||||||
import { useEffect, useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import * as THREE from 'three/webgpu'
|
import * as THREE from 'three/webgpu'
|
||||||
import useViewer from '../../store/use-viewer'
|
import useViewer from '../../store/use-viewer'
|
||||||
import { GuideSystem } from '../../systems/guide/guide-system'
|
import { GuideSystem } from '../../systems/guide/guide-system'
|
||||||
@@ -62,23 +62,14 @@ extend(THREE as any)
|
|||||||
interface ViewerProps {
|
interface ViewerProps {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
selectionManager?: 'default' | 'custom'
|
selectionManager?: 'default' | 'custom'
|
||||||
isEditor?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Viewer: React.FC<ViewerProps> = ({
|
const Viewer: React.FC<ViewerProps> = ({
|
||||||
children,
|
children,
|
||||||
selectionManager = 'default',
|
selectionManager = 'default',
|
||||||
isEditor = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
const setIsEditor = useViewer((state) => state.setIsEditor)
|
|
||||||
const theme = useViewer((state) => state.theme)
|
const theme = useViewer((state) => state.theme)
|
||||||
|
|
||||||
const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa'
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsEditor(isEditor)
|
|
||||||
}, [isEditor, setIsEditor])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
dpr={[1, 1.5]}
|
dpr={[1, 1.5]}
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ type Outliner = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ViewerState = {
|
type ViewerState = {
|
||||||
isEditor: boolean
|
|
||||||
setIsEditor: (isEditor: boolean) => void
|
|
||||||
|
|
||||||
selection: SelectionPath
|
selection: SelectionPath
|
||||||
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
||||||
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
|
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
|
||||||
@@ -74,8 +71,6 @@ type ViewerState = {
|
|||||||
const useViewer = create<ViewerState>()(
|
const useViewer = create<ViewerState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
isEditor: false,
|
|
||||||
setIsEditor: (isEditor) => set({ isEditor }),
|
|
||||||
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||||
hoveredId: null,
|
hoveredId: null,
|
||||||
setHoveredId: (id) => set({ hoveredId: id }),
|
setHoveredId: (id) => set({ hoveredId: id }),
|
||||||
|
|||||||
Reference in New Issue
Block a user