fix tools visibility
This commit is contained in:
@@ -2,8 +2,9 @@ import { CeilingNode, emitter, type GridEvent, type LevelNode, useScene } from '
|
|||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
|
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
|
||||||
import useEditor from '@/store/use-editor'
|
|
||||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
|
||||||
const CEILING_HEIGHT = 2.52
|
const CEILING_HEIGHT = 2.52
|
||||||
const GRID_OFFSET = 0.02
|
const GRID_OFFSET = 0.02
|
||||||
@@ -97,12 +98,19 @@ export const CeilingTool: React.FC = () => {
|
|||||||
|
|
||||||
// Calculate snapped display position (bypass snap when Shift is held)
|
// Calculate snapped display position (bypass snap when Shift is held)
|
||||||
const lastPoint = points[points.length - 1]
|
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)
|
setSnappedCursorPosition(displayPoint)
|
||||||
|
|
||||||
// Play snap sound when the snapped position actually changes (only when drawing)
|
// Play snap sound when the snapped position actually changes (only when drawing)
|
||||||
if (points.length > 0 && previousSnappedPointRef.current &&
|
if (
|
||||||
(displayPoint[0] !== previousSnappedPointRef.current[0] || displayPoint[1] !== previousSnappedPointRef.current[1])) {
|
points.length > 0 &&
|
||||||
|
previousSnappedPointRef.current &&
|
||||||
|
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
||||||
|
displayPoint[1] !== previousSnappedPointRef.current[1])
|
||||||
|
) {
|
||||||
sfxEmitter.emit('sfx:grid-snap')
|
sfxEmitter.emit('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +158,12 @@ export const CeilingTool: React.FC = () => {
|
|||||||
setPoints([])
|
setPoints([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = true }
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
const onKeyUp = (e: KeyboardEvent) => { if (e.key === 'Shift') shiftPressed.current = false }
|
if (e.key === 'Shift') shiftPressed.current = true
|
||||||
|
}
|
||||||
|
const onKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Shift') shiftPressed.current = false
|
||||||
|
}
|
||||||
document.addEventListener('keydown', onKeyDown)
|
document.addEventListener('keydown', onKeyDown)
|
||||||
document.addEventListener('keyup', onKeyUp)
|
document.addEventListener('keyup', onKeyUp)
|
||||||
|
|
||||||
@@ -242,15 +254,12 @@ export const CeilingTool: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Cursor at ceiling height */}
|
{/* Cursor at ceiling height */}
|
||||||
<mesh ref={cursorRef}>
|
<CursorSphere ref={cursorRef} />
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
|
||||||
<meshBasicMaterial color="#d4d4d4" depthTest={false} depthWrite={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* Grid-level cursor indicator */}
|
{/* Grid-level cursor indicator */}
|
||||||
<mesh ref={gridCursorRef} rotation={[-Math.PI / 2, 0, 0]}>
|
<mesh ref={gridCursorRef} rotation={[-Math.PI / 2, 0, 0]} renderOrder={2}>
|
||||||
<ringGeometry args={[0.15, 0.2, 32]} />
|
<ringGeometry args={[0.15, 0.2, 32]} />
|
||||||
<meshBasicMaterial color="#a3a3a3" side={DoubleSide} depthTest={false} depthWrite={false} />
|
<meshBasicMaterial color="#a3a3a3" side={DoubleSide} depthTest={false} depthWrite={true} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Preview fill */}
|
{/* Preview fill */}
|
||||||
@@ -294,14 +303,11 @@ export const CeilingTool: React.FC = () => {
|
|||||||
|
|
||||||
{/* Point markers */}
|
{/* Point markers */}
|
||||||
{points.map(([x, z], index) => (
|
{points.map(([x, z], index) => (
|
||||||
<mesh key={index} position={[x, levelY + CEILING_HEIGHT + 0.01, z]}>
|
<CursorSphere
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
key={index}
|
||||||
<meshBasicMaterial
|
position={[x, levelY + CEILING_HEIGHT + 0.01, z]}
|
||||||
color={index === 0 ? '#22c55e' : '#d4d4d4'}
|
color={index === 0 ? '#22c55e' : undefined}
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
/>
|
||||||
</mesh>
|
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,37 +1,44 @@
|
|||||||
import { emitter, type GridEvent, useScene, RoofNode, type LevelNode, type AnyNode } from "@pascal-app/core";
|
import {
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
type AnyNode,
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
emitter,
|
||||||
import { BufferGeometry, DoubleSide, type Line, Vector3 } from "three";
|
type GridEvent,
|
||||||
import useEditor from "@/store/use-editor";
|
type LevelNode,
|
||||||
import { sfxEmitter } from '@/lib/sfx-bus';
|
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
|
// Default roof dimensions
|
||||||
const DEFAULT_HEIGHT = 1.5;
|
const DEFAULT_HEIGHT = 1.5
|
||||||
const PREVIEW_LINE_HEIGHT = 0.03; // Very thin preview
|
const PREVIEW_LINE_HEIGHT = 0.03 // Very thin preview
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a roof with the given corners
|
* Creates a roof with the given corners
|
||||||
*/
|
*/
|
||||||
const commitRoofPlacement = (
|
const commitRoofPlacement = (
|
||||||
levelId: LevelNode["id"],
|
levelId: LevelNode['id'],
|
||||||
corner1: [number, number, number],
|
corner1: [number, number, number],
|
||||||
corner2: [number, number, number]
|
corner2: [number, number, number],
|
||||||
): RoofNode["id"] => {
|
): RoofNode['id'] => {
|
||||||
const { createNode, nodes } = useScene.getState();
|
const { createNode, nodes } = useScene.getState()
|
||||||
|
|
||||||
// Calculate center position and dimensions from corners
|
// Calculate center position and dimensions from corners
|
||||||
const centerX = (corner1[0] + corner2[0]) / 2;
|
const centerX = (corner1[0] + corner2[0]) / 2
|
||||||
const centerZ = (corner1[2] + corner2[2]) / 2;
|
const centerZ = (corner1[2] + corner2[2]) / 2
|
||||||
|
|
||||||
const length = Math.abs(corner2[0] - corner1[0]);
|
const length = Math.abs(corner2[0] - corner1[0])
|
||||||
const width = Math.abs(corner2[2] - corner1[2]);
|
const width = Math.abs(corner2[2] - corner1[2])
|
||||||
|
|
||||||
// Split width evenly between left and right slopes
|
// 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
|
// Count existing roofs for naming
|
||||||
const roofCount = Object.values(nodes).filter((n) => n.type === "roof").length;
|
const roofCount = Object.values(nodes).filter((n) => n.type === 'roof').length
|
||||||
const name = `Roof ${roofCount + 1}`;
|
const name = `Roof ${roofCount + 1}`
|
||||||
|
|
||||||
const roof = RoofNode.parse({
|
const roof = RoofNode.parse({
|
||||||
name,
|
name,
|
||||||
@@ -40,151 +47,153 @@ const commitRoofPlacement = (
|
|||||||
height: DEFAULT_HEIGHT,
|
height: DEFAULT_HEIGHT,
|
||||||
leftWidth: slopeWidth,
|
leftWidth: slopeWidth,
|
||||||
rightWidth: slopeWidth,
|
rightWidth: slopeWidth,
|
||||||
});
|
})
|
||||||
|
|
||||||
createNode(roof, levelId);
|
createNode(roof, levelId)
|
||||||
sfxEmitter.emit('sfx:structure-build');
|
sfxEmitter.emit('sfx:structure-build')
|
||||||
return roof.id;
|
return roof.id
|
||||||
};
|
}
|
||||||
|
|
||||||
type PreviewState = {
|
type PreviewState = {
|
||||||
corner1: [number, number, number] | null;
|
corner1: [number, number, number] | null
|
||||||
cursorPosition: [number, number, number];
|
cursorPosition: [number, number, number]
|
||||||
levelY: number;
|
levelY: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export const RoofTool: React.FC = () => {
|
export const RoofTool: React.FC = () => {
|
||||||
const outlineRef = useRef<Line>(null!);
|
const outlineRef = useRef<Line>(null!)
|
||||||
const currentLevelId = useViewer((state) => state.selection.levelId);
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
||||||
const setSelection = useViewer((state) => state.setSelection);
|
const setSelection = useViewer((state) => state.setSelection)
|
||||||
const setTool = useEditor((state) => state.setTool);
|
const setTool = useEditor((state) => state.setTool)
|
||||||
const setMode = useEditor((state) => state.setMode);
|
const setMode = useEditor((state) => state.setMode)
|
||||||
|
|
||||||
const corner1Ref = useRef<[number, number, number] | null>(null);
|
const corner1Ref = useRef<[number, number, number] | null>(null)
|
||||||
const previousGridPosRef = useRef<[number, number] | null>(null);
|
const previousGridPosRef = useRef<[number, number] | null>(null)
|
||||||
const [preview, setPreview] = useState<PreviewState>({
|
const [preview, setPreview] = useState<PreviewState>({
|
||||||
corner1: null,
|
corner1: null,
|
||||||
cursorPosition: [0, 0, 0],
|
cursorPosition: [0, 0, 0],
|
||||||
levelY: 0,
|
levelY: 0,
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentLevelId) return;
|
if (!currentLevelId) return
|
||||||
|
|
||||||
// Initialize outline geometry
|
// Initialize outline geometry
|
||||||
outlineRef.current.geometry = new BufferGeometry();
|
outlineRef.current.geometry = new BufferGeometry()
|
||||||
|
|
||||||
const updateOutline = (corner1: [number, number, number], corner2: [number, number, number]) => {
|
const updateOutline = (
|
||||||
const y = corner1[1] + PREVIEW_LINE_HEIGHT;
|
corner1: [number, number, number],
|
||||||
|
corner2: [number, number, number],
|
||||||
|
) => {
|
||||||
|
const y = corner1[1] + PREVIEW_LINE_HEIGHT
|
||||||
const points = [
|
const points = [
|
||||||
new Vector3(corner1[0], y, corner1[2]),
|
new Vector3(corner1[0], y, corner1[2]),
|
||||||
new Vector3(corner2[0], y, corner1[2]),
|
new Vector3(corner2[0], y, corner1[2]),
|
||||||
new Vector3(corner2[0], y, corner2[2]),
|
new Vector3(corner2[0], y, corner2[2]),
|
||||||
new Vector3(corner1[0], y, corner2[2]),
|
new Vector3(corner1[0], y, corner2[2]),
|
||||||
new Vector3(corner1[0], y, corner1[2]), // Close the loop
|
new Vector3(corner1[0], y, corner1[2]), // Close the loop
|
||||||
];
|
]
|
||||||
outlineRef.current.geometry.dispose();
|
outlineRef.current.geometry.dispose()
|
||||||
outlineRef.current.geometry = new BufferGeometry().setFromPoints(points);
|
outlineRef.current.geometry = new BufferGeometry().setFromPoints(points)
|
||||||
outlineRef.current.visible = true;
|
outlineRef.current.visible = true
|
||||||
};
|
}
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
// Snap to 0.5 grid
|
// Snap to 0.5 grid
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2;
|
const gridX = Math.round(event.position[0] * 2) / 2
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2;
|
const gridZ = Math.round(event.position[2] * 2) / 2
|
||||||
const y = event.position[1];
|
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)
|
// Play snap sound when grid position changes (only when placing)
|
||||||
if (corner1Ref.current && previousGridPosRef.current &&
|
if (
|
||||||
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) {
|
corner1Ref.current &&
|
||||||
sfxEmitter.emit('sfx:grid-snap');
|
previousGridPosRef.current &&
|
||||||
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
||||||
|
) {
|
||||||
|
sfxEmitter.emit('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
|
|
||||||
previousGridPosRef.current = [gridX, gridZ];
|
previousGridPosRef.current = [gridX, gridZ]
|
||||||
|
|
||||||
setPreview({
|
setPreview({
|
||||||
corner1: corner1Ref.current,
|
corner1: corner1Ref.current,
|
||||||
cursorPosition,
|
cursorPosition,
|
||||||
levelY: y,
|
levelY: y,
|
||||||
});
|
})
|
||||||
|
|
||||||
// Update outline if we have first corner
|
// Update outline if we have first corner
|
||||||
if (corner1Ref.current) {
|
if (corner1Ref.current) {
|
||||||
updateOutline(corner1Ref.current, cursorPosition);
|
updateOutline(corner1Ref.current, cursorPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
if (!currentLevelId) return;
|
if (!currentLevelId) return
|
||||||
|
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2;
|
const gridX = Math.round(event.position[0] * 2) / 2
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2;
|
const gridZ = Math.round(event.position[2] * 2) / 2
|
||||||
const y = event.position[1];
|
const y = event.position[1]
|
||||||
|
|
||||||
if (!corner1Ref.current) {
|
if (!corner1Ref.current) {
|
||||||
// First click - set corner 1
|
// First click - set corner 1
|
||||||
corner1Ref.current = [gridX, y, gridZ];
|
corner1Ref.current = [gridX, y, gridZ]
|
||||||
setPreview((prev) => ({
|
setPreview((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
corner1: corner1Ref.current,
|
corner1: corner1Ref.current,
|
||||||
}));
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Second click - create the roof
|
// Second click - create the roof
|
||||||
const roofId = commitRoofPlacement(
|
const roofId = commitRoofPlacement(currentLevelId, corner1Ref.current, [gridX, y, gridZ])
|
||||||
currentLevelId,
|
|
||||||
corner1Ref.current,
|
|
||||||
[gridX, y, gridZ]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Auto-select the newly created roof
|
// Auto-select the newly created roof
|
||||||
setSelection({ selectedIds: [roofId as AnyNode["id"]] });
|
setSelection({ selectedIds: [roofId as AnyNode['id']] })
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
corner1Ref.current = null;
|
corner1Ref.current = null
|
||||||
outlineRef.current.visible = false;
|
outlineRef.current.visible = false
|
||||||
|
|
||||||
// Switch to select mode and deactivate tool
|
// Switch to select mode and deactivate tool
|
||||||
setMode('select');
|
setMode('select')
|
||||||
setTool(null);
|
setTool(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
if (corner1Ref.current) {
|
if (corner1Ref.current) {
|
||||||
corner1Ref.current = null;
|
corner1Ref.current = null
|
||||||
outlineRef.current.visible = false;
|
outlineRef.current.visible = false
|
||||||
setPreview((prev) => ({ ...prev, corner1: null }));
|
setPreview((prev) => ({ ...prev, corner1: null }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Subscribe to events
|
// Subscribe to events
|
||||||
emitter.on("grid:move", onGridMove);
|
emitter.on('grid:move', onGridMove)
|
||||||
emitter.on("grid:click", onGridClick);
|
emitter.on('grid:click', onGridClick)
|
||||||
emitter.on("tool:cancel", onCancel);
|
emitter.on('tool:cancel', onCancel)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off("grid:move", onGridMove);
|
emitter.off('grid:move', onGridMove)
|
||||||
emitter.off("grid:click", onGridClick);
|
emitter.off('grid:click', onGridClick)
|
||||||
emitter.off("tool:cancel", onCancel);
|
emitter.off('tool:cancel', onCancel)
|
||||||
|
|
||||||
// Reset state on unmount
|
// Reset state on unmount
|
||||||
corner1Ref.current = null;
|
corner1Ref.current = null
|
||||||
};
|
}
|
||||||
}, [currentLevelId, setTool, setSelection, setMode]);
|
}, [currentLevelId, setTool, setSelection, setMode])
|
||||||
|
|
||||||
const { corner1, cursorPosition, levelY } = preview;
|
const { corner1, cursorPosition, levelY } = preview
|
||||||
|
|
||||||
// Calculate preview dimensions for display
|
// Calculate preview dimensions for display
|
||||||
const previewDimensions = useMemo(() => {
|
const previewDimensions = useMemo(() => {
|
||||||
if (!corner1) return null;
|
if (!corner1) return null
|
||||||
const length = Math.abs(cursorPosition[0] - corner1[0]);
|
const length = Math.abs(cursorPosition[0] - corner1[0])
|
||||||
const width = Math.abs(cursorPosition[2] - corner1[2]);
|
const width = Math.abs(cursorPosition[2] - corner1[2])
|
||||||
const centerX = (corner1[0] + cursorPosition[0]) / 2;
|
const centerX = (corner1[0] + cursorPosition[0]) / 2
|
||||||
const centerZ = (corner1[2] + cursorPosition[2]) / 2;
|
const centerZ = (corner1[2] + cursorPosition[2]) / 2
|
||||||
return { length, width, centerX, centerZ };
|
return { length, width, centerX, centerZ }
|
||||||
}, [corner1, cursorPosition]);
|
}, [corner1, cursorPosition])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -192,34 +201,25 @@ export const RoofTool: React.FC = () => {
|
|||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<line ref={outlineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
<line ref={outlineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
||||||
<bufferGeometry />
|
<bufferGeometry />
|
||||||
<lineBasicNodeMaterial
|
<lineBasicNodeMaterial color="#8b4513" linewidth={2} depthTest={false} depthWrite={false} />
|
||||||
color="#8b4513"
|
|
||||||
linewidth={2}
|
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
|
||||||
</line>
|
</line>
|
||||||
|
|
||||||
{/* First corner marker */}
|
{/* First corner marker */}
|
||||||
{corner1 && (
|
{corner1 && (
|
||||||
<mesh position={[corner1[0], levelY + 0.02, corner1[2]]} rotation={[-Math.PI / 2, 0, 0]}>
|
<mesh position={[corner1[0], levelY + 0.02, corner1[2]]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={2}>
|
||||||
<ringGeometry args={[0.1, 0.15, 32]} />
|
<ringGeometry args={[0.1, 0.15, 32]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial color="#22c55e" depthTest={false} depthWrite={true} />
|
||||||
color="#22c55e"
|
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cursor marker on ground */}
|
{/* Cursor marker on ground */}
|
||||||
<mesh position={[cursorPosition[0], cursorPosition[1] + 0.02, cursorPosition[2]]} rotation={[-Math.PI / 2, 0, 0]}>
|
<mesh
|
||||||
|
position={[cursorPosition[0], cursorPosition[1] + 0.02, cursorPosition[2]]}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
renderOrder={2}
|
||||||
|
>
|
||||||
<ringGeometry args={[0.1, 0.15, 32]} />
|
<ringGeometry args={[0.1, 0.15, 32]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial color="#8b4513" depthTest={false} depthWrite={true} />
|
||||||
color="#8b4513"
|
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Thin preview fill when drawing */}
|
{/* Thin preview fill when drawing */}
|
||||||
@@ -240,5 +240,5 @@ export const RoofTool: React.FC = () => {
|
|||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { forwardRef } from 'react'
|
||||||
|
import type { Mesh } from 'three'
|
||||||
|
|
||||||
|
interface CursorSphereProps extends Omit<JSX.IntrinsicElements['mesh'], 'ref'> {
|
||||||
|
color?: string
|
||||||
|
depthWrite?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CursorSphere = forwardRef<Mesh, CursorSphereProps>(function CursorSphere(
|
||||||
|
{ color = '#f1c066', ...props },
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<mesh ref={ref} {...props} renderOrder={2}>
|
||||||
|
<sphereGeometry args={[0.1, 16, 16]} />
|
||||||
|
<meshBasicMaterial color={color} depthTest={false} depthWrite={true} />
|
||||||
|
</mesh>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
|
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from 'three'
|
||||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||||
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
|
||||||
const Y_OFFSET = 0.02
|
const Y_OFFSET = 0.02
|
||||||
|
|
||||||
@@ -236,10 +237,7 @@ export const SlabTool: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Cursor */}
|
{/* Cursor */}
|
||||||
<mesh ref={cursorRef}>
|
<CursorSphere ref={cursorRef} />
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
|
||||||
<meshBasicMaterial color="#a3a3a3" depthTest={false} depthWrite={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* Preview fill */}
|
{/* Preview fill */}
|
||||||
{previewShape && (
|
{previewShape && (
|
||||||
@@ -282,14 +280,7 @@ export const SlabTool: React.FC = () => {
|
|||||||
|
|
||||||
{/* Point markers */}
|
{/* Point markers */}
|
||||||
{points.map(([x, z], index) => (
|
{points.map(([x, z], index) => (
|
||||||
<mesh key={index} position={[x, levelY + Y_OFFSET + 0.01, z]}>
|
<CursorSphere key={index} position={[x, levelY + Y_OFFSET + 0.01, z]} color={index === 0 ? '#22c55e' : undefined} />
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color={index === 0 ? '#22c55e' : '#a3a3a3'}
|
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { DoubleSide, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three'
|
import { DoubleSide, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three'
|
||||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||||
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
|
||||||
const WALL_HEIGHT = 2.5
|
const WALL_HEIGHT = 2.5
|
||||||
const WALL_THICKNESS = 0.15
|
const WALL_THICKNESS = 0.15
|
||||||
@@ -183,10 +184,7 @@ export const WallTool: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Cursor indicator */}
|
{/* Cursor indicator */}
|
||||||
<mesh ref={cursorRef}>
|
<CursorSphere ref={cursorRef} />
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
|
||||||
<meshBasicMaterial color="#a3a3a3" depthTest={false} depthWrite={false} />
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* Wall preview */}
|
{/* Wall preview */}
|
||||||
<mesh ref={wallPreviewRef} visible={false} renderOrder={1}>
|
<mesh ref={wallPreviewRef} visible={false} renderOrder={1}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useViewer } from "@pascal-app/viewer";
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from "three";
|
import { BufferGeometry, DoubleSide, type Line, type Mesh, Shape, Vector3 } from "three";
|
||||||
import useEditor from "@/store/use-editor";
|
import useEditor from "@/store/use-editor";
|
||||||
|
import { CursorSphere } from "../shared/cursor-sphere";
|
||||||
|
|
||||||
// Zone colors for cycling through
|
// Zone colors for cycling through
|
||||||
const ZONE_COLORS = [
|
const ZONE_COLORS = [
|
||||||
@@ -317,14 +318,7 @@ export const ZoneTool: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Cursor */}
|
{/* Cursor */}
|
||||||
<mesh ref={cursorRef}>
|
<CursorSphere ref={cursorRef} color="#3b82f6" />
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color="#3b82f6"
|
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
|
|
||||||
{/* Preview fill */}
|
{/* Preview fill */}
|
||||||
{previewShape && (
|
{previewShape && (
|
||||||
@@ -373,14 +367,7 @@ export const ZoneTool: React.FC = () => {
|
|||||||
{/* Point markers */}
|
{/* Point markers */}
|
||||||
{points.map(([x, z], index) =>
|
{points.map(([x, z], index) =>
|
||||||
isValidPoint([x, z]) ? (
|
isValidPoint([x, z]) ? (
|
||||||
<mesh key={index} position={[x, levelY + Y_OFFSET + 0.01, z]}>
|
<CursorSphere key={index} position={[x, levelY + Y_OFFSET + 0.01, z]} color={index === 0 ? "#22c55e" : "#3b82f6"} />
|
||||||
<sphereGeometry args={[0.1, 16, 16]} />
|
|
||||||
<meshBasicMaterial
|
|
||||||
color={index === 0 ? "#22c55e" : "#3b82f6"}
|
|
||||||
depthTest={false}
|
|
||||||
depthWrite={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
) : null
|
) : null
|
||||||
)}
|
)}
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type CeilingNode, useRegistry } from '@pascal-app/core'
|
import { type CeilingNode, useRegistry } from '@pascal-app/core'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { faceDirection, float, mix, positionWorld, smoothstep, step } from 'three/tsl'
|
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 { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
import { NodeRenderer } from '../node-renderer'
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ import { NodeRenderer } from '../node-renderer'
|
|||||||
// - Front face (looking down at ceiling from above): 30% opacity
|
// - Front face (looking down at ceiling from above): 30% opacity
|
||||||
const ceilingMaterial = new MeshBasicNodeMaterial({
|
const ceilingMaterial = new MeshBasicNodeMaterial({
|
||||||
color: 0x999999,
|
color: 0x999999,
|
||||||
side: DoubleSide,
|
|
||||||
transparent: true,
|
transparent: true,
|
||||||
depthWrite: false,
|
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
|
// Combine: if either X or Y is a line, show the line
|
||||||
const gridPattern = lineX.max(lineY)
|
const gridPattern = lineX.max(lineY)
|
||||||
|
|
||||||
// Grid lines at 0.8 opacity, spaces at 0.1 opacity
|
// Grid lines at 0.5 opacity, spaces at 0 opacity
|
||||||
const gridOpacity = mix(float(0.1), float(0.8), gridPattern)
|
const gridOpacity = mix(float(0.0), float(0.5), gridPattern)
|
||||||
|
|
||||||
// faceDirection is 1.0 for front face, -1.0 for back face
|
// 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
|
// Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid
|
||||||
|
|||||||
Reference in New Issue
Block a user