From 14708c041a4cce96b9719756111af2a5dc56ebaa Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 24 Feb 2026 16:09:03 +0900 Subject: [PATCH] fix tools visibility --- .../components/tools/ceiling/ceiling-tool.tsx | 46 ++-- .../components/tools/roof/roof-tool.tsx | 228 +++++++++--------- .../components/tools/shared/cursor-sphere.tsx | 19 ++ .../components/tools/slab/slab-tool.tsx | 15 +- .../components/tools/wall/wall-tool.tsx | 6 +- .../components/tools/zone/zone-tool.tsx | 19 +- .../renderers/ceiling/ceiling-renderer.tsx | 7 +- 7 files changed, 170 insertions(+), 170 deletions(-) create mode 100644 apps/editor/components/tools/shared/cursor-sphere.tsx diff --git a/apps/editor/components/tools/ceiling/ceiling-tool.tsx b/apps/editor/components/tools/ceiling/ceiling-tool.tsx index 3a56a74b..89567f85 100644 --- a/apps/editor/components/tools/ceiling/ceiling-tool.tsx +++ b/apps/editor/components/tools/ceiling/ceiling-tool.tsx @@ -2,8 +2,9 @@ import { CeilingNode, emitter, type GridEvent, type LevelNode, useScene } from ' import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three' -import useEditor from '@/store/use-editor' import { sfxEmitter } from '@/lib/sfx-bus' +import useEditor from '@/store/use-editor' +import { CursorSphere } from '../shared/cursor-sphere' const CEILING_HEIGHT = 2.52 const GRID_OFFSET = 0.02 @@ -97,12 +98,19 @@ export const CeilingTool: React.FC = () => { // Calculate snapped display position (bypass snap when Shift is held) const lastPoint = points[points.length - 1] - const displayPoint = (shiftPressed.current || !lastPoint) ? gridPosition : calculateSnapPoint(lastPoint, gridPosition) + const displayPoint = + shiftPressed.current || !lastPoint + ? gridPosition + : calculateSnapPoint(lastPoint, gridPosition) setSnappedCursorPosition(displayPoint) // Play snap sound when the snapped position actually changes (only when drawing) - if (points.length > 0 && previousSnappedPointRef.current && - (displayPoint[0] !== previousSnappedPointRef.current[0] || displayPoint[1] !== previousSnappedPointRef.current[1])) { + if ( + points.length > 0 && + previousSnappedPointRef.current && + (displayPoint[0] !== previousSnappedPointRef.current[0] || + displayPoint[1] !== previousSnappedPointRef.current[1]) + ) { sfxEmitter.emit('sfx:grid-snap') } @@ -150,8 +158,12 @@ export const CeilingTool: React.FC = () => { setPoints([]) } - const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = true } - const onKeyUp = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = false } + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftPressed.current = true + } + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') shiftPressed.current = false + } document.addEventListener('keydown', onKeyDown) document.addEventListener('keyup', onKeyUp) @@ -242,15 +254,12 @@ export const CeilingTool: React.FC = () => { return ( {/* Cursor at ceiling height */} - - - - + {/* Grid-level cursor indicator */} - + - + {/* Preview fill */} @@ -294,14 +303,11 @@ export const CeilingTool: React.FC = () => { {/* Point markers */} {points.map(([x, z], index) => ( - - - - + ))} ) diff --git a/apps/editor/components/tools/roof/roof-tool.tsx b/apps/editor/components/tools/roof/roof-tool.tsx index 9f351007..6698ae75 100644 --- a/apps/editor/components/tools/roof/roof-tool.tsx +++ b/apps/editor/components/tools/roof/roof-tool.tsx @@ -1,37 +1,44 @@ -import { emitter, type GridEvent, useScene, RoofNode, type LevelNode, type AnyNode } from "@pascal-app/core"; -import { useViewer } from "@pascal-app/viewer"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { BufferGeometry, DoubleSide, type Line, Vector3 } from "three"; -import useEditor from "@/store/use-editor"; -import { sfxEmitter } from '@/lib/sfx-bus'; +import { + type AnyNode, + emitter, + type GridEvent, + type LevelNode, + RoofNode, + useScene, +} from '@pascal-app/core' +import { useViewer } from '@pascal-app/viewer' +import { useEffect, useMemo, useRef, useState } from 'react' +import { BufferGeometry, DoubleSide, type Line, Vector3 } from 'three' +import { sfxEmitter } from '@/lib/sfx-bus' +import useEditor from '@/store/use-editor' // Default roof dimensions -const DEFAULT_HEIGHT = 1.5; -const PREVIEW_LINE_HEIGHT = 0.03; // Very thin preview +const DEFAULT_HEIGHT = 1.5 +const PREVIEW_LINE_HEIGHT = 0.03 // Very thin preview /** * Creates a roof with the given corners */ const commitRoofPlacement = ( - levelId: LevelNode["id"], + levelId: LevelNode['id'], corner1: [number, number, number], - corner2: [number, number, number] -): RoofNode["id"] => { - const { createNode, nodes } = useScene.getState(); + corner2: [number, number, number], +): RoofNode['id'] => { + const { createNode, nodes } = useScene.getState() // Calculate center position and dimensions from corners - const centerX = (corner1[0] + corner2[0]) / 2; - const centerZ = (corner1[2] + corner2[2]) / 2; + const centerX = (corner1[0] + corner2[0]) / 2 + const centerZ = (corner1[2] + corner2[2]) / 2 - const length = Math.abs(corner2[0] - corner1[0]); - const width = Math.abs(corner2[2] - corner1[2]); + const length = Math.abs(corner2[0] - corner1[0]) + const width = Math.abs(corner2[2] - corner1[2]) // Split width evenly between left and right slopes - const slopeWidth = Math.max(width / 2, 0.5); + const slopeWidth = Math.max(width / 2, 0.5) // Count existing roofs for naming - const roofCount = Object.values(nodes).filter((n) => n.type === "roof").length; - const name = `Roof ${roofCount + 1}`; + const roofCount = Object.values(nodes).filter((n) => n.type === 'roof').length + const name = `Roof ${roofCount + 1}` const roof = RoofNode.parse({ name, @@ -40,151 +47,153 @@ const commitRoofPlacement = ( height: DEFAULT_HEIGHT, leftWidth: slopeWidth, rightWidth: slopeWidth, - }); + }) - createNode(roof, levelId); - sfxEmitter.emit('sfx:structure-build'); - return roof.id; -}; + createNode(roof, levelId) + sfxEmitter.emit('sfx:structure-build') + return roof.id +} type PreviewState = { - corner1: [number, number, number] | null; - cursorPosition: [number, number, number]; - levelY: number; -}; + corner1: [number, number, number] | null + cursorPosition: [number, number, number] + levelY: number +} export const RoofTool: React.FC = () => { - const outlineRef = useRef(null!); - const currentLevelId = useViewer((state) => state.selection.levelId); - const setSelection = useViewer((state) => state.setSelection); - const setTool = useEditor((state) => state.setTool); - const setMode = useEditor((state) => state.setMode); + const outlineRef = useRef(null!) + const currentLevelId = useViewer((state) => state.selection.levelId) + const setSelection = useViewer((state) => state.setSelection) + const setTool = useEditor((state) => state.setTool) + const setMode = useEditor((state) => state.setMode) - const corner1Ref = useRef<[number, number, number] | null>(null); - const previousGridPosRef = useRef<[number, number] | null>(null); + const corner1Ref = useRef<[number, number, number] | null>(null) + const previousGridPosRef = useRef<[number, number] | null>(null) const [preview, setPreview] = useState({ corner1: null, cursorPosition: [0, 0, 0], levelY: 0, - }); + }) useEffect(() => { - if (!currentLevelId) return; + if (!currentLevelId) return // Initialize outline geometry - outlineRef.current.geometry = new BufferGeometry(); + outlineRef.current.geometry = new BufferGeometry() - const updateOutline = (corner1: [number, number, number], corner2: [number, number, number]) => { - const y = corner1[1] + PREVIEW_LINE_HEIGHT; + const updateOutline = ( + corner1: [number, number, number], + corner2: [number, number, number], + ) => { + const y = corner1[1] + PREVIEW_LINE_HEIGHT const points = [ new Vector3(corner1[0], y, corner1[2]), new Vector3(corner2[0], y, corner1[2]), new Vector3(corner2[0], y, corner2[2]), new Vector3(corner1[0], y, corner2[2]), new Vector3(corner1[0], y, corner1[2]), // Close the loop - ]; - outlineRef.current.geometry.dispose(); - outlineRef.current.geometry = new BufferGeometry().setFromPoints(points); - outlineRef.current.visible = true; - }; + ] + outlineRef.current.geometry.dispose() + outlineRef.current.geometry = new BufferGeometry().setFromPoints(points) + outlineRef.current.visible = true + } const onGridMove = (event: GridEvent) => { // Snap to 0.5 grid - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - const y = event.position[1]; + const gridX = Math.round(event.position[0] * 2) / 2 + const gridZ = Math.round(event.position[2] * 2) / 2 + const y = event.position[1] - const cursorPosition: [number, number, number] = [gridX, y, gridZ]; + const cursorPosition: [number, number, number] = [gridX, y, gridZ] // Play snap sound when grid position changes (only when placing) - if (corner1Ref.current && previousGridPosRef.current && - (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) { - sfxEmitter.emit('sfx:grid-snap'); + if ( + corner1Ref.current && + previousGridPosRef.current && + (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1]) + ) { + sfxEmitter.emit('sfx:grid-snap') } - previousGridPosRef.current = [gridX, gridZ]; + previousGridPosRef.current = [gridX, gridZ] setPreview({ corner1: corner1Ref.current, cursorPosition, levelY: y, - }); + }) // Update outline if we have first corner if (corner1Ref.current) { - updateOutline(corner1Ref.current, cursorPosition); + updateOutline(corner1Ref.current, cursorPosition) } - }; + } const onGridClick = (event: GridEvent) => { - if (!currentLevelId) return; + if (!currentLevelId) return - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - const y = event.position[1]; + const gridX = Math.round(event.position[0] * 2) / 2 + const gridZ = Math.round(event.position[2] * 2) / 2 + const y = event.position[1] if (!corner1Ref.current) { // First click - set corner 1 - corner1Ref.current = [gridX, y, gridZ]; + corner1Ref.current = [gridX, y, gridZ] setPreview((prev) => ({ ...prev, corner1: corner1Ref.current, - })); + })) } else { // Second click - create the roof - const roofId = commitRoofPlacement( - currentLevelId, - corner1Ref.current, - [gridX, y, gridZ] - ); + const roofId = commitRoofPlacement(currentLevelId, corner1Ref.current, [gridX, y, gridZ]) // Auto-select the newly created roof - setSelection({ selectedIds: [roofId as AnyNode["id"]] }); + setSelection({ selectedIds: [roofId as AnyNode['id']] }) // Reset state - corner1Ref.current = null; - outlineRef.current.visible = false; + corner1Ref.current = null + outlineRef.current.visible = false // Switch to select mode and deactivate tool - setMode('select'); - setTool(null); + setMode('select') + setTool(null) } - }; + } const onCancel = () => { if (corner1Ref.current) { - corner1Ref.current = null; - outlineRef.current.visible = false; - setPreview((prev) => ({ ...prev, corner1: null })); + corner1Ref.current = null + outlineRef.current.visible = false + setPreview((prev) => ({ ...prev, corner1: null })) } - }; + } // Subscribe to events - emitter.on("grid:move", onGridMove); - emitter.on("grid:click", onGridClick); - emitter.on("tool:cancel", onCancel); + emitter.on('grid:move', onGridMove) + emitter.on('grid:click', onGridClick) + emitter.on('tool:cancel', onCancel) return () => { - emitter.off("grid:move", onGridMove); - emitter.off("grid:click", onGridClick); - emitter.off("tool:cancel", onCancel); + emitter.off('grid:move', onGridMove) + emitter.off('grid:click', onGridClick) + emitter.off('tool:cancel', onCancel) // Reset state on unmount - corner1Ref.current = null; - }; - }, [currentLevelId, setTool, setSelection, setMode]); + corner1Ref.current = null + } + }, [currentLevelId, setTool, setSelection, setMode]) - const { corner1, cursorPosition, levelY } = preview; + const { corner1, cursorPosition, levelY } = preview // Calculate preview dimensions for display const previewDimensions = useMemo(() => { - if (!corner1) return null; - const length = Math.abs(cursorPosition[0] - corner1[0]); - const width = Math.abs(cursorPosition[2] - corner1[2]); - const centerX = (corner1[0] + cursorPosition[0]) / 2; - const centerZ = (corner1[2] + cursorPosition[2]) / 2; - return { length, width, centerX, centerZ }; - }, [corner1, cursorPosition]); + if (!corner1) return null + const length = Math.abs(cursorPosition[0] - corner1[0]) + const width = Math.abs(cursorPosition[2] - corner1[2]) + const centerX = (corner1[0] + cursorPosition[0]) / 2 + const centerZ = (corner1[2] + cursorPosition[2]) / 2 + return { length, width, centerX, centerZ } + }, [corner1, cursorPosition]) return ( @@ -192,34 +201,25 @@ export const RoofTool: React.FC = () => { {/* @ts-ignore */} - + {/* First corner marker */} {corner1 && ( - + - + )} {/* Cursor marker on ground */} - + - + {/* Thin preview fill when drawing */} @@ -240,5 +240,5 @@ export const RoofTool: React.FC = () => { )} - ); -}; + ) +} diff --git a/apps/editor/components/tools/shared/cursor-sphere.tsx b/apps/editor/components/tools/shared/cursor-sphere.tsx new file mode 100644 index 00000000..641edbe8 --- /dev/null +++ b/apps/editor/components/tools/shared/cursor-sphere.tsx @@ -0,0 +1,19 @@ +import { forwardRef } from 'react' +import type { Mesh } from 'three' + +interface CursorSphereProps extends Omit { + color?: string + depthWrite?: boolean +} + +export const CursorSphere = forwardRef(function CursorSphere( + { color = '#f1c066', ...props }, + ref, +) { + return ( + + + + + ) +}) diff --git a/apps/editor/components/tools/slab/slab-tool.tsx b/apps/editor/components/tools/slab/slab-tool.tsx index facdc8de..7f88f54f 100644 --- a/apps/editor/components/tools/slab/slab-tool.tsx +++ b/apps/editor/components/tools/slab/slab-tool.tsx @@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer' import { useEffect, useMemo, useRef, useState } from 'react' import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three' import { sfxEmitter } from '@/lib/sfx-bus' +import { CursorSphere } from '../shared/cursor-sphere' const Y_OFFSET = 0.02 @@ -236,10 +237,7 @@ export const SlabTool: React.FC = () => { return ( {/* Cursor */} - - - - + {/* Preview fill */} {previewShape && ( @@ -282,14 +280,7 @@ export const SlabTool: React.FC = () => { {/* Point markers */} {points.map(([x, z], index) => ( - - - - + ))} ) diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx index fa51d65b..72755cb0 100644 --- a/apps/editor/components/tools/wall/wall-tool.tsx +++ b/apps/editor/components/tools/wall/wall-tool.tsx @@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer' import { useEffect, useRef } from 'react' import { DoubleSide, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three' import { sfxEmitter } from '@/lib/sfx-bus' +import { CursorSphere } from '../shared/cursor-sphere' const WALL_HEIGHT = 2.5 const WALL_THICKNESS = 0.15 @@ -183,10 +184,7 @@ export const WallTool: React.FC = () => { return ( {/* Cursor indicator */} - - - - + {/* Wall preview */} diff --git a/apps/editor/components/tools/zone/zone-tool.tsx b/apps/editor/components/tools/zone/zone-tool.tsx index c9af47b9..dfc5dae7 100644 --- a/apps/editor/components/tools/zone/zone-tool.tsx +++ b/apps/editor/components/tools/zone/zone-tool.tsx @@ -3,6 +3,7 @@ import { useViewer } from "@pascal-app/viewer"; import { useEffect, useMemo, useRef, useState } from "react"; import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from "three"; import useEditor from "@/store/use-editor"; +import { CursorSphere } from "../shared/cursor-sphere"; // Zone colors for cycling through const ZONE_COLORS = [ @@ -317,14 +318,7 @@ export const ZoneTool: React.FC = () => { return ( {/* Cursor */} - - - - + {/* Preview fill */} {previewShape && ( @@ -373,14 +367,7 @@ export const ZoneTool: React.FC = () => { {/* Point markers */} {points.map(([x, z], index) => isValidPoint([x, z]) ? ( - - - - + ) : null )} diff --git a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx index 7094a4d9..ee86c9a5 100644 --- a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx +++ b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx @@ -1,7 +1,7 @@ import { type CeilingNode, useRegistry } from '@pascal-app/core' import { useRef } from 'react' import { faceDirection, float, mix, positionWorld, smoothstep, step } from 'three/tsl' -import { DoubleSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu' +import { type Mesh, MeshBasicNodeMaterial } from 'three/webgpu' import { useNodeEvents } from '../../../hooks/use-node-events' import { NodeRenderer } from '../node-renderer' @@ -10,7 +10,6 @@ import { NodeRenderer } from '../node-renderer' // - Front face (looking down at ceiling from above): 30% opacity const ceilingMaterial = new MeshBasicNodeMaterial({ color: 0x999999, - side: DoubleSide, transparent: true, depthWrite: false, }) @@ -30,8 +29,8 @@ const lineY = smoothstep(lineWidth, 0, gridY).add(smoothstep(1.0 - lineWidth, 1. // Combine: if either X or Y is a line, show the line const gridPattern = lineX.max(lineY) -// Grid lines at 0.8 opacity, spaces at 0.1 opacity -const gridOpacity = mix(float(0.1), float(0.8), gridPattern) +// Grid lines at 0.5 opacity, spaces at 0 opacity +const gridOpacity = mix(float(0.0), float(0.5), gridPattern) // faceDirection is 1.0 for front face, -1.0 for back face // Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid