remove isEditor for site-edge-labels

This commit is contained in:
wass08
2026-03-02 08:35:16 +09:00
parent f1d0d3a78c
commit d81a48d64e
4 changed files with 139 additions and 89 deletions
@@ -1,6 +1,14 @@
'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 { Html } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
@@ -18,7 +26,6 @@ export function FloatingActionMenu() {
const deleteNode = useScene((s) => s.deleteNode)
const setMovingNode = useEditor((s) => s.setMovingNode)
const setSelection = useViewer((s) => s.setSelection)
const isEditor = useViewer((state) => state.isEditor)
const groupRef = useRef<THREE.Group>(null)
@@ -42,61 +49,70 @@ export function FloatingActionMenu() {
}
})
const handleMove = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
if (!node) return
sfxEmitter.emit('sfx:item-pick')
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)
const handleMove = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
if (!node) return
sfxEmitter.emit('sfx:item-pick')
if (node.type === 'item' || node.type === 'window' || node.type === 'door') {
setMovingNode(node as any)
}
setSelection({ selectedIds: [] })
}
}, [node, setMovingNode, setSelection])
},
[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])
const handleDuplicate = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
if (!node || !node.parentId) return
sfxEmitter.emit('sfx:item-pick')
useScene.temporal.getState().pause()
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 (
<group ref={groupRef}>
@@ -105,10 +121,10 @@ export function FloatingActionMenu() {
zIndexRange={[100, 0]}
style={{
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"
onPointerDown={(e) => e.stopPropagation()}
onPointerUp={(e) => e.stopPropagation()}
+2
View File
@@ -24,6 +24,7 @@ import { ExportManager } from './export-manager'
import { FloatingActionMenu } from './floating-action-menu'
import { Grid } from './grid'
import { SelectionManager } from './selection-manager'
import { SiteEdgeLabels } from './site-edge-labels'
import { ThumbnailGenerator } from './thumbnail-generator'
// Load default scene initially (will be replaced when project loads)
@@ -118,6 +119,7 @@ export default function Editor({ projectId }: EditorProps) {
<ToolManager />
<CustomCameraControls />
<ThumbnailGenerator projectId={projectId} />
<SiteEdgeLabels />
</Viewer>
</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,
)
}