ceiling rendering + tools fix

This commit is contained in:
wass08
2026-01-27 09:36:11 +09:00
parent e3e3602d91
commit 150a0a4ca1
5 changed files with 55 additions and 37 deletions
@@ -4,7 +4,8 @@ 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";
const Y_OFFSET = 2.52; // Slightly above default ceiling height const CEILING_HEIGHT = 2.52; // Slightly above default ceiling height
const GRID_OFFSET = 0.02; // Small offset above floor level
/** /**
* Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point * Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
@@ -69,6 +70,7 @@ const commitCeilingDrawing = (
type PreviewState = { type PreviewState = {
points: Array<[number, number]>; points: Array<[number, number]>;
cursorPoint: [number, number] | null; cursorPoint: [number, number] | null;
levelY: number;
}; };
// Helper to validate point values (no NaN or Infinity) // Helper to validate point values (no NaN or Infinity)
@@ -79,14 +81,13 @@ const isValidPoint = (
return Number.isFinite(pt[0]) && Number.isFinite(pt[1]); return Number.isFinite(pt[0]) && Number.isFinite(pt[1]);
}; };
const GRID_Y = 0.02; // Grid level indicator
export const CeilingTool: React.FC = () => { export const CeilingTool: React.FC = () => {
const cursorRef = useRef<Mesh>(null); const cursorRef = useRef<Mesh>(null);
const gridCursorRef = useRef<Mesh>(null); const gridCursorRef = useRef<Mesh>(null);
const mainLineRef = useRef<Line>(null!); const mainLineRef = useRef<Line>(null!);
const closingLineRef = useRef<Line>(null!); const closingLineRef = useRef<Line>(null!);
const pointsRef = useRef<Array<[number, number]>>([]); const pointsRef = useRef<Array<[number, number]>>([]);
const levelYRef = useRef(0); // Track current level Y position
const currentLevelId = useViewer((state) => state.selection.levelId); const currentLevelId = useViewer((state) => state.selection.levelId);
const setTool = useEditor((state) => state.setTool); const setTool = useEditor((state) => state.setTool);
@@ -94,6 +95,7 @@ export const CeilingTool: React.FC = () => {
const [preview, setPreview] = useState<PreviewState>({ const [preview, setPreview] = useState<PreviewState>({
points: [], points: [],
cursorPoint: null, cursorPoint: null,
levelY: 0,
}); });
useEffect(() => { useEffect(() => {
@@ -107,6 +109,7 @@ export const CeilingTool: React.FC = () => {
const updateLines = () => { const updateLines = () => {
const points = pointsRef.current; const points = pointsRef.current;
const ceilingY = levelYRef.current + CEILING_HEIGHT;
if (points.length === 0) { if (points.length === 0) {
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
@@ -116,7 +119,7 @@ export const CeilingTool: React.FC = () => {
// Build main line points // Build main line points
const linePoints: Vector3[] = points.map( const linePoints: Vector3[] = points.map(
([x, z]) => new Vector3(x, Y_OFFSET, z) ([x, z]) => new Vector3(x, ceilingY, z)
); );
// Add cursor point // Add cursor point
@@ -124,7 +127,7 @@ export const CeilingTool: React.FC = () => {
if (lastPoint) { if (lastPoint) {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
if (isValidPoint(snapped)) { if (isValidPoint(snapped)) {
linePoints.push(new Vector3(snapped[0], Y_OFFSET, snapped[1])); linePoints.push(new Vector3(snapped[0], ceilingY, snapped[1]));
} }
} }
@@ -143,8 +146,8 @@ export const CeilingTool: React.FC = () => {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
if (isValidPoint(snapped)) { if (isValidPoint(snapped)) {
const closingPoints = [ const closingPoints = [
new Vector3(snapped[0], Y_OFFSET, snapped[1]), new Vector3(snapped[0], ceilingY, snapped[1]),
new Vector3(firstPoint[0], Y_OFFSET, firstPoint[1]), new Vector3(firstPoint[0], ceilingY, firstPoint[1]),
]; ];
closingLineRef.current.geometry.dispose(); closingLineRef.current.geometry.dispose();
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints);
@@ -166,7 +169,7 @@ export const CeilingTool: React.FC = () => {
cursorPt = cursorPosition; cursorPt = cursorPosition;
} }
setPreview({ points: [...points], cursorPoint: cursorPt }); setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current });
updateLines(); updateLines();
}; };
@@ -177,20 +180,24 @@ export const CeilingTool: React.FC = () => {
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;
cursorPosition = [gridX, gridZ]; cursorPosition = [gridX, gridZ];
levelYRef.current = event.position[1];
const ceilingY = event.position[1] + CEILING_HEIGHT;
const gridY = event.position[1] + GRID_OFFSET;
// If we have points, snap to axis from last point // If we have points, snap to axis from last point
const lastPoint = pointsRef.current[pointsRef.current.length - 1]; const lastPoint = pointsRef.current[pointsRef.current.length - 1];
if (lastPoint) { if (lastPoint) {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
cursorRef.current.position.set(snapped[0], Y_OFFSET, snapped[1]); cursorRef.current.position.set(snapped[0], ceilingY, snapped[1]);
// Also update grid-level cursor // Also update grid-level cursor
if (gridCursorRef.current) { if (gridCursorRef.current) {
gridCursorRef.current.position.set(snapped[0], GRID_Y, snapped[1]); gridCursorRef.current.position.set(snapped[0], gridY, snapped[1]);
} }
} else { } else {
cursorRef.current.position.set(gridX, Y_OFFSET, gridZ); cursorRef.current.position.set(gridX, ceilingY, gridZ);
if (gridCursorRef.current) { if (gridCursorRef.current) {
gridCursorRef.current.position.set(gridX, GRID_Y, gridZ); gridCursorRef.current.position.set(gridX, gridY, gridZ);
} }
} }
@@ -223,7 +230,7 @@ export const CeilingTool: React.FC = () => {
// Reset state // Reset state
pointsRef.current = []; pointsRef.current = [];
setPreview({ points: [], cursorPoint: null }); setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
closingLineRef.current.visible = false; closingLineRef.current.visible = false;
@@ -245,7 +252,7 @@ export const CeilingTool: React.FC = () => {
// Reset state // Reset state
pointsRef.current = []; pointsRef.current = [];
setPreview({ points: [], cursorPoint: null }); setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
closingLineRef.current.visible = false; closingLineRef.current.visible = false;
@@ -269,7 +276,7 @@ export const CeilingTool: React.FC = () => {
}; };
}, [currentLevelId, setTool]); }, [currentLevelId, setTool]);
const { points, cursorPoint } = preview; const { points, cursorPoint, levelY } = preview;
// Create preview shape when we have 3+ points // Create preview shape when we have 3+ points
const previewShape = useMemo(() => { const previewShape = useMemo(() => {
@@ -327,7 +334,7 @@ export const CeilingTool: React.FC = () => {
{previewShape && ( {previewShape && (
<mesh <mesh
frustumCulled={false} frustumCulled={false}
position={[0, Y_OFFSET, 0]} position={[0, levelY + CEILING_HEIGHT, 0]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
> >
<shapeGeometry args={[previewShape]} /> <shapeGeometry args={[previewShape]} />
@@ -370,7 +377,7 @@ export const CeilingTool: 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, Y_OFFSET + 0.01, z]}> <mesh key={index} position={[x, levelY + CEILING_HEIGHT + 0.01, z]}>
<sphereGeometry args={[0.1, 16, 16]} /> <sphereGeometry args={[0.1, 16, 16]} />
<meshBasicMaterial <meshBasicMaterial
color={index === 0 ? "#22c55e" : "#d4d4d4"} color={index === 0 ? "#22c55e" : "#d4d4d4"}
@@ -9,6 +9,7 @@ import {
type WallEvent, type WallEvent,
type WallNode, type WallNode,
} from '@pascal-app/core' } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import { useFrame } from '@react-three/fiber' import { useFrame } from '@react-three/fiber'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
@@ -4,7 +4,7 @@ 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";
const Y_OFFSET = 0.02; const Y_OFFSET = 0.02; // Small offset above floor level
/** /**
* Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point * Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
@@ -69,6 +69,7 @@ const commitSlabDrawing = (
type PreviewState = { type PreviewState = {
points: Array<[number, number]>; points: Array<[number, number]>;
cursorPoint: [number, number] | null; cursorPoint: [number, number] | null;
levelY: number;
}; };
// Helper to validate point values (no NaN or Infinity) // Helper to validate point values (no NaN or Infinity)
@@ -84,6 +85,7 @@ export const SlabTool: React.FC = () => {
const mainLineRef = useRef<Line>(null!); const mainLineRef = useRef<Line>(null!);
const closingLineRef = useRef<Line>(null!); const closingLineRef = useRef<Line>(null!);
const pointsRef = useRef<Array<[number, number]>>([]); const pointsRef = useRef<Array<[number, number]>>([]);
const levelYRef = useRef(0); // Track current level Y position
const currentLevelId = useViewer((state) => state.selection.levelId); const currentLevelId = useViewer((state) => state.selection.levelId);
const setTool = useEditor((state) => state.setTool); const setTool = useEditor((state) => state.setTool);
@@ -91,6 +93,7 @@ export const SlabTool: React.FC = () => {
const [preview, setPreview] = useState<PreviewState>({ const [preview, setPreview] = useState<PreviewState>({
points: [], points: [],
cursorPoint: null, cursorPoint: null,
levelY: 0,
}); });
useEffect(() => { useEffect(() => {
@@ -104,6 +107,7 @@ export const SlabTool: React.FC = () => {
const updateLines = () => { const updateLines = () => {
const points = pointsRef.current; const points = pointsRef.current;
const y = levelYRef.current + Y_OFFSET;
if (points.length === 0) { if (points.length === 0) {
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
@@ -113,7 +117,7 @@ export const SlabTool: React.FC = () => {
// Build main line points // Build main line points
const linePoints: Vector3[] = points.map( const linePoints: Vector3[] = points.map(
([x, z]) => new Vector3(x, Y_OFFSET, z) ([x, z]) => new Vector3(x, y, z)
); );
// Add cursor point // Add cursor point
@@ -121,7 +125,7 @@ export const SlabTool: React.FC = () => {
if (lastPoint) { if (lastPoint) {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
if (isValidPoint(snapped)) { if (isValidPoint(snapped)) {
linePoints.push(new Vector3(snapped[0], Y_OFFSET, snapped[1])); linePoints.push(new Vector3(snapped[0], y, snapped[1]));
} }
} }
@@ -140,8 +144,8 @@ export const SlabTool: React.FC = () => {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
if (isValidPoint(snapped)) { if (isValidPoint(snapped)) {
const closingPoints = [ const closingPoints = [
new Vector3(snapped[0], Y_OFFSET, snapped[1]), new Vector3(snapped[0], y, snapped[1]),
new Vector3(firstPoint[0], Y_OFFSET, firstPoint[1]), new Vector3(firstPoint[0], y, firstPoint[1]),
]; ];
closingLineRef.current.geometry.dispose(); closingLineRef.current.geometry.dispose();
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints);
@@ -163,7 +167,7 @@ export const SlabTool: React.FC = () => {
cursorPt = cursorPosition; cursorPt = cursorPosition;
} }
setPreview({ points: [...points], cursorPoint: cursorPt }); setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current });
updateLines(); updateLines();
}; };
@@ -174,6 +178,7 @@ export const SlabTool: React.FC = () => {
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;
cursorPosition = [gridX, gridZ]; cursorPosition = [gridX, gridZ];
levelYRef.current = event.position[1];
// If we have points, snap to axis from last point // If we have points, snap to axis from last point
const lastPoint = pointsRef.current[pointsRef.current.length - 1]; const lastPoint = pointsRef.current[pointsRef.current.length - 1];
@@ -259,7 +264,7 @@ export const SlabTool: React.FC = () => {
}; };
}, [currentLevelId, setTool]); }, [currentLevelId, setTool]);
const { points, cursorPoint } = preview; const { points, cursorPoint, levelY } = preview;
// Create preview shape when we have 3+ points // Create preview shape when we have 3+ points
const previewShape = useMemo(() => { const previewShape = useMemo(() => {
@@ -306,7 +311,7 @@ export const SlabTool: React.FC = () => {
{previewShape && ( {previewShape && (
<mesh <mesh
frustumCulled={false} frustumCulled={false}
position={[0, Y_OFFSET, 0]} position={[0, levelY + Y_OFFSET, 0]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
> >
<shapeGeometry args={[previewShape]} /> <shapeGeometry args={[previewShape]} />
@@ -349,7 +354,7 @@ export const SlabTool: 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, Y_OFFSET + 0.01, z]}> <mesh key={index} position={[x, levelY + Y_OFFSET + 0.01, z]}>
<sphereGeometry args={[0.1, 16, 16]} /> <sphereGeometry args={[0.1, 16, 16]} />
<meshBasicMaterial <meshBasicMaterial
color={index === 0 ? "#22c55e" : "#a3a3a3"} color={index === 0 ? "#22c55e" : "#a3a3a3"}
+15 -10
View File
@@ -88,6 +88,7 @@ const commitZoneDrawing = (
type PreviewState = { type PreviewState = {
points: Array<[number, number]>; points: Array<[number, number]>;
cursorPoint: [number, number] | null; cursorPoint: [number, number] | null;
levelY: number;
}; };
// Helper to validate point values (no NaN or Infinity) // Helper to validate point values (no NaN or Infinity)
@@ -103,6 +104,7 @@ export const ZoneTool: React.FC = () => {
const mainLineRef = useRef<Line>(null!); const mainLineRef = useRef<Line>(null!);
const closingLineRef = useRef<Line>(null!); const closingLineRef = useRef<Line>(null!);
const pointsRef = useRef<Array<[number, number]>>([]); const pointsRef = useRef<Array<[number, number]>>([]);
const levelYRef = useRef(0); // Track current level Y position
const currentLevelId = useViewer((state) => state.selection.levelId); const currentLevelId = useViewer((state) => state.selection.levelId);
const setTool = useEditor((state) => state.setTool); const setTool = useEditor((state) => state.setTool);
@@ -110,6 +112,7 @@ export const ZoneTool: React.FC = () => {
const [preview, setPreview] = useState<PreviewState>({ const [preview, setPreview] = useState<PreviewState>({
points: [], points: [],
cursorPoint: null, cursorPoint: null,
levelY: 0,
}); });
useEffect(() => { useEffect(() => {
@@ -123,6 +126,7 @@ export const ZoneTool: React.FC = () => {
const updateLines = () => { const updateLines = () => {
const points = pointsRef.current; const points = pointsRef.current;
const y = levelYRef.current + Y_OFFSET;
if (points.length === 0) { if (points.length === 0) {
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
@@ -132,7 +136,7 @@ export const ZoneTool: React.FC = () => {
// Build main line points // Build main line points
const linePoints: Vector3[] = points.map( const linePoints: Vector3[] = points.map(
([x, z]) => new Vector3(x, Y_OFFSET, z) ([x, z]) => new Vector3(x, y, z)
); );
// Add cursor point // Add cursor point
@@ -140,7 +144,7 @@ export const ZoneTool: React.FC = () => {
if (lastPoint) { if (lastPoint) {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
if (isValidPoint(snapped)) { if (isValidPoint(snapped)) {
linePoints.push(new Vector3(snapped[0], Y_OFFSET, snapped[1])); linePoints.push(new Vector3(snapped[0], y, snapped[1]));
} }
} }
@@ -159,8 +163,8 @@ export const ZoneTool: React.FC = () => {
const snapped = calculateSnapPoint(lastPoint, cursorPosition); const snapped = calculateSnapPoint(lastPoint, cursorPosition);
if (isValidPoint(snapped)) { if (isValidPoint(snapped)) {
const closingPoints = [ const closingPoints = [
new Vector3(snapped[0], Y_OFFSET, snapped[1]), new Vector3(snapped[0], y, snapped[1]),
new Vector3(firstPoint[0], Y_OFFSET, firstPoint[1]), new Vector3(firstPoint[0], y, firstPoint[1]),
]; ];
closingLineRef.current.geometry.dispose(); closingLineRef.current.geometry.dispose();
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints);
@@ -182,7 +186,7 @@ export const ZoneTool: React.FC = () => {
cursorPt = cursorPosition; cursorPt = cursorPosition;
} }
setPreview({ points: [...points], cursorPoint: cursorPt }); setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current });
updateLines(); updateLines();
}; };
@@ -193,6 +197,7 @@ export const ZoneTool: React.FC = () => {
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;
cursorPosition = [gridX, gridZ]; cursorPosition = [gridX, gridZ];
levelYRef.current = event.position[1];
// If we have points, snap to axis from last point // If we have points, snap to axis from last point
const lastPoint = pointsRef.current[pointsRef.current.length - 1]; const lastPoint = pointsRef.current[pointsRef.current.length - 1];
@@ -232,7 +237,7 @@ export const ZoneTool: React.FC = () => {
// Reset state // Reset state
pointsRef.current = []; pointsRef.current = [];
setPreview({ points: [], cursorPoint: null }); setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
closingLineRef.current.visible = false; closingLineRef.current.visible = false;
@@ -254,7 +259,7 @@ export const ZoneTool: React.FC = () => {
// Reset state // Reset state
pointsRef.current = []; pointsRef.current = [];
setPreview({ points: [], cursorPoint: null }); setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
mainLineRef.current.visible = false; mainLineRef.current.visible = false;
closingLineRef.current.visible = false; closingLineRef.current.visible = false;
@@ -278,7 +283,7 @@ export const ZoneTool: React.FC = () => {
}; };
}, [currentLevelId, setTool]); }, [currentLevelId, setTool]);
const { points, cursorPoint } = preview; const { points, cursorPoint, levelY } = preview;
// Create preview shape when we have 3+ points // Create preview shape when we have 3+ points
const previewShape = useMemo(() => { const previewShape = useMemo(() => {
@@ -325,7 +330,7 @@ export const ZoneTool: React.FC = () => {
{previewShape && ( {previewShape && (
<mesh <mesh
frustumCulled={false} frustumCulled={false}
position={[0, Y_OFFSET, 0]} position={[0, levelY + Y_OFFSET, 0]}
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
> >
<shapeGeometry args={[previewShape]} /> <shapeGeometry args={[previewShape]} />
@@ -368,7 +373,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, Y_OFFSET + 0.01, z]}> <mesh key={index} position={[x, levelY + Y_OFFSET + 0.01, z]}>
<sphereGeometry args={[0.1, 16, 16]} /> <sphereGeometry args={[0.1, 16, 16]} />
<meshBasicMaterial <meshBasicMaterial
color={index === 0 ? "#22c55e" : "#3b82f6"} color={index === 0 ? "#22c55e" : "#3b82f6"}
@@ -40,7 +40,7 @@ function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) {
mesh.geometry = newGeo mesh.geometry = newGeo
// Position at the ceiling height // Position at the ceiling height
mesh.position.y = node.height ?? 2.5 mesh.position.y = (node.height ?? 2.5) - 0.01 // Slight offset to avoid z-fighting with upper-level slabs
} }
/** /**