From d81a48d64e609b6d4af29f256710882b8b3ac9ba Mon Sep 17 00:00:00 2001 From: wass08 Date: Mon, 2 Mar 2026 08:35:16 +0900 Subject: [PATCH 1/3] remove isEditor for site-edge-labels --- .../editor/floating-action-menu.tsx | 126 ++++++++++-------- apps/editor/components/editor/index.tsx | 2 + .../components/editor/site-edge-labels.tsx | 66 +++++++++ .../renderers/site/site-renderer.tsx | 34 ----- 4 files changed, 139 insertions(+), 89 deletions(-) create mode 100644 apps/editor/components/editor/site-edge-labels.tsx diff --git a/apps/editor/components/editor/floating-action-menu.tsx b/apps/editor/components/editor/floating-action-menu.tsx index 26f0853d..d5667f56 100644 --- a/apps/editor/components/editor/floating-action-menu.tsx +++ b/apps/editor/components/editor/floating-action-menu.tsx @@ -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(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 ( @@ -105,10 +121,10 @@ export function FloatingActionMenu() { zIndexRange={[100, 0]} style={{ pointerEvents: 'auto', - touchAction: 'none' + touchAction: 'none', }} > -
e.stopPropagation()} onPointerUp={(e) => e.stopPropagation()} diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 161d4ddb..669e6894 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -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) { +
) diff --git a/apps/editor/components/editor/site-edge-labels.tsx b/apps/editor/components/editor/site-edge-labels.tsx new file mode 100644 index 00000000..1af576ba --- /dev/null +++ b/apps/editor/components/editor/site-edge-labels.tsx @@ -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(null) + const prevSiteNodeIdRef = useRef(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) => ( + +
+ {edge.dist.toFixed(2)}m +
+ + ))} + , + siteObj, + ) +} diff --git a/packages/viewer/src/components/renderers/site/site-renderer.tsx b/packages/viewer/src/components/renderers/site/site-renderer.tsx index d1d01144..bd384d03 100644 --- a/packages/viewer/src/components/renderers/site/site-renderer.tsx +++ b/packages/viewer/src/components/renderers/site/site-renderer.tsx @@ -1,13 +1,10 @@ import { type SiteNode, useRegistry } from '@pascal-app/core' -import { Html } from '@react-three/drei' import { useMemo, useRef } from 'react' import { BufferGeometry, Float32BufferAttribute, type Group, Shape } from 'three' import { useNodeEvents } from '../../../hooks/use-node-events' -import useViewer from '../../../store/use-viewer' import { NodeRenderer } from '../node-renderer' const Y_OFFSET = 0.01 -const LINE_HEIGHT = 0.5 /** * Creates simple line geometry for site boundary @@ -62,22 +59,6 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { return createBoundaryLineGeometry(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') if (!node || !floorShape || !lineGeometry) { @@ -106,21 +87,6 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { - {/* Edge distance labels */} - {isEditor && edges.map((edge, i) => ( - -
- {edge.dist.toFixed(2)}m -
- - ))}
) } From 882d3fec7e0ed2d8ff7b072f4301b855c40bf715 Mon Sep 17 00:00:00 2001 From: wass08 Date: Mon, 2 Mar 2026 08:50:53 +0900 Subject: [PATCH 2/3] remove is editor completely --- apps/editor/components/editor/index.tsx | 3 +- .../renderers/zone/zone-renderer.tsx | 109 ++---------------- .../viewer/src/components/viewer/index.tsx | 11 +- packages/viewer/src/store/use-viewer.ts | 5 - 4 files changed, 13 insertions(+), 115 deletions(-) diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index 669e6894..ec095031 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -27,6 +27,7 @@ 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) useScene.getState().loadScene() initSpatialGridSync() @@ -107,7 +108,7 @@ export default function Editor({ projectId }: EditorProps) { - + diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx index bbf9a81f..315d5f60 100644 --- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx +++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx @@ -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 { useMemo, useRef, useState, useEffect } from 'react' +import { useMemo, useRef } from 'react' import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three' import { color, float, uniform, uv } from 'three/tsl' import { MeshBasicNodeMaterial } from 'three/webgpu' import { useNodeEvents } from '../../../hooks/use-node-events' -import useViewer from '../../../store/use-viewer' const Y_OFFSET = 0.01 const WALL_HEIGHT = 2.3 @@ -105,43 +104,6 @@ const createWallGeometry = (polygon: Array<[number, number]>): BufferGeometry => export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { const ref = useRef(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(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) @@ -215,71 +177,20 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { } return ( - - + +
isEditor && setIsHovered(true)} - onMouseLeave={() => isEditor && setIsHovered(false)} - onClick={(e) => { - if (isEditor && !isEditing) { - e.stopPropagation() - setIsEditing(true) - } }}> - {isEditing ? ( - 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()} - /> - ) : ( - <> - {node.name} - {isEditor && ( -
- - - - -
- )} - - )} + {node.name}
{/* Floor fill */} diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx index 2b144668..4840125c 100644 --- a/packages/viewer/src/components/viewer/index.tsx +++ b/packages/viewer/src/components/viewer/index.tsx @@ -11,7 +11,7 @@ import { } from '@pascal-app/core' import { Bvh } from '@react-three/drei' 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 useViewer from '../../store/use-viewer' import { GuideSystem } from '../../systems/guide/guide-system' @@ -62,23 +62,14 @@ extend(THREE as any) interface ViewerProps { children?: React.ReactNode selectionManager?: 'default' | 'custom' - isEditor?: boolean } const Viewer: React.FC = ({ children, selectionManager = 'default', - isEditor = false, }) => { - const setIsEditor = useViewer((state) => state.setIsEditor) const theme = useViewer((state) => state.theme) - const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa' - - useEffect(() => { - setIsEditor(isEditor) - }, [isEditor, setIsEditor]) - return ( void - selection: SelectionPath hoveredId: AnyNode['id'] | ZoneNode['id'] | null setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void @@ -74,8 +71,6 @@ type ViewerState = { const useViewer = create()( persist( (set) => ({ - isEditor: false, - setIsEditor: (isEditor) => set({ isEditor }), selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] }, hoveredId: null, setHoveredId: (id) => set({ hoveredId: id }), From f17e0cb955f07e50892e7a6e88d551cb4cd6b476 Mon Sep 17 00:00:00 2001 From: wass08 Date: Mon, 2 Mar 2026 08:53:34 +0900 Subject: [PATCH 3/3] fix dot color --- apps/editor/components/ui/sidebar/panels/site-panel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx index 9b96eb36..bf40beeb 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx @@ -958,7 +958,7 @@ function ZoneItem({ zone, isLast }: { zone: ZoneNode, isLast?: boolean }) {