Merge pull request #128 from pascalorg/fix/remove-is-editor

Fix/remove is editor
This commit is contained in:
Wassim SAMAD
2026-03-02 08:55:10 +09:00
committed by GitHub
8 changed files with 153 additions and 205 deletions
@@ -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,7 +49,8 @@ export function FloatingActionMenu() {
} }
}) })
const handleMove = useCallback((e: React.MouseEvent) => { const handleMove = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (!node) return if (!node) return
sfxEmitter.emit('sfx:item-pick') sfxEmitter.emit('sfx:item-pick')
@@ -50,9 +58,12 @@ export function FloatingActionMenu() {
setMovingNode(node as any) setMovingNode(node as any)
} }
setSelection({ selectedIds: [] }) setSelection({ selectedIds: [] })
}, [node, setMovingNode, setSelection]) },
[node, setMovingNode, setSelection],
)
const handleDuplicate = useCallback((e: React.MouseEvent) => { const handleDuplicate = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (!node || !node.parentId) return if (!node || !node.parentId) return
sfxEmitter.emit('sfx:item-pick') sfxEmitter.emit('sfx:item-pick')
@@ -85,18 +96,23 @@ export function FloatingActionMenu() {
} }
setSelection({ selectedIds: [] }) setSelection({ selectedIds: [] })
} }
}, [node, setMovingNode, setSelection]) },
[node, setMovingNode, setSelection],
)
const handleDelete = useCallback((e: React.MouseEvent) => { const handleDelete = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (!selectedId || !node) return if (!selectedId || !node) return
sfxEmitter.emit('sfx:item-delete') sfxEmitter.emit('sfx:item-delete')
deleteNode(selectedId as AnyNodeId) deleteNode(selectedId as AnyNodeId)
if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId) if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
setSelection({ selectedIds: [] }) setSelection({ selectedIds: [] })
}, [selectedId, node, deleteNode, setSelection]) },
[selectedId, node, deleteNode, setSelection],
)
if (!isEditor || !selectedId || !node || !isValidType) return null if (!selectedId || !node || !isValidType) return null
return ( return (
<group ref={groupRef}> <group ref={groupRef}>
@@ -105,7 +121,7 @@ export function FloatingActionMenu() {
zIndexRange={[100, 0]} zIndexRange={[100, 0]}
style={{ style={{
pointerEvents: 'auto', pointerEvents: 'auto',
touchAction: 'none' touchAction: 'none',
}} }}
> >
<div <div
+4 -1
View File
@@ -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 ? (
<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> <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]}
-5
View File
@@ -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 }),