Use raycast cursor & level Y fallback
Ceiling and slab tools: switch cursor tracking to per-frame raycasting onto a level plane (useFrame) instead of relying on grid:move events, and memoize Raycaster/Plane to reduce allocations. Update line geometry updates to be performed imperatively from the frame loop, ensure previews only trigger React updates when the snapped display position changes, and keep cursor/preview snapping consistent with user-visible pointer. Add sceneRegistry-based fallback to resolve a level's Y position (used by Grid, CeilingTool and SlabTool) when the level mesh isn't registered yet, computing Y from level node data and viewer levelMode. Misc: import/add necessary three.js types and small refactors to visibility/reset logic.
This commit is contained in:
@@ -148,6 +148,15 @@ const Grid = ({
|
|||||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
||||||
if (levelMesh) {
|
if (levelMesh) {
|
||||||
targetY = levelMesh.position.y
|
targetY = levelMesh.position.y
|
||||||
|
} else {
|
||||||
|
// Fallback: compute from level node data when mesh isn't registered yet
|
||||||
|
const levelNode = useScene.getState().nodes[currentLevelId]
|
||||||
|
if (levelNode && 'level' in levelNode) {
|
||||||
|
const levelMode = useViewer.getState().levelMode
|
||||||
|
const LEVEL_HEIGHT = 2.5
|
||||||
|
const EXPLODED_GAP = 5
|
||||||
|
targetY = ((levelNode as any).level || 0) * (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gridRef.current.position.y = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta)
|
gridRef.current.position.y = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { emitter, type GridEvent, useScene, CeilingNode, type LevelNode } from "@pascal-app/core";
|
import { emitter, type GridEvent, useScene, CeilingNode, type LevelNode, sceneRegistry } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
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, Plane, Raycaster, Shape, Vector3 } from "three";
|
||||||
import useEditor from "@/store/use-editor";
|
import useEditor from "@/store/use-editor";
|
||||||
|
|
||||||
const CEILING_HEIGHT = 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
|
const GRID_OFFSET = 0.02; // Small offset above floor level
|
||||||
|
const UP = new Vector3(0, 1, 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -87,10 +89,17 @@ export const CeilingTool: 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 levelYRef = useRef(0);
|
||||||
|
const cursorPositionRef = useRef<[number, number]>([0, 0]);
|
||||||
|
const lastDisplayRef = useRef<{ x: number; z: number } | null>(null);
|
||||||
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);
|
||||||
|
|
||||||
|
// Reusable objects for raycasting (created once, avoid per-frame allocations)
|
||||||
|
const raycaster = useMemo(() => new Raycaster(), []);
|
||||||
|
const levelPlane = useMemo(() => new Plane(UP, 0), []);
|
||||||
|
const hitPoint = useMemo(() => new Vector3(), []);
|
||||||
|
|
||||||
// Preview state for reactive rendering (for shape and point markers)
|
// Preview state for reactive rendering (for shape and point markers)
|
||||||
const [preview, setPreview] = useState<PreviewState>({
|
const [preview, setPreview] = useState<PreviewState>({
|
||||||
points: [],
|
points: [],
|
||||||
@@ -98,17 +107,27 @@ export const CeilingTool: React.FC = () => {
|
|||||||
levelY: 0,
|
levelY: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Resolve the current level's Y position from scene registry or node data
|
||||||
if (!currentLevelId) return;
|
const getLevelY = (): number => {
|
||||||
|
if (!currentLevelId) return 0;
|
||||||
let cursorPosition: [number, number] = [0, 0];
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||||
|
if (levelMesh) return levelMesh.position.y;
|
||||||
// Initialize line geometries
|
const levelNode = useScene.getState().nodes[currentLevelId];
|
||||||
mainLineRef.current.geometry = new BufferGeometry();
|
if (levelNode && 'level' in levelNode) {
|
||||||
closingLineRef.current.geometry = new BufferGeometry();
|
const levelMode = useViewer.getState().levelMode;
|
||||||
|
const LEVEL_HEIGHT = 2.5;
|
||||||
|
const EXPLODED_GAP = 5;
|
||||||
|
return ((levelNode as any).level || 0) * (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Imperatively update line geometries (called from useFrame)
|
||||||
const updateLines = () => {
|
const updateLines = () => {
|
||||||
|
if (!mainLineRef.current || !closingLineRef.current) return;
|
||||||
|
|
||||||
const points = pointsRef.current;
|
const points = pointsRef.current;
|
||||||
|
const cursorPosition = cursorPositionRef.current;
|
||||||
const ceilingY = levelYRef.current + CEILING_HEIGHT;
|
const ceilingY = levelYRef.current + CEILING_HEIGHT;
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
@@ -158,58 +177,84 @@ export const CeilingTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePreview = () => {
|
// Per-frame cursor positioning via direct raycasting onto the level plane.
|
||||||
const points = pointsRef.current;
|
// This bypasses R3F's event propagation (grid:move), ensuring the cursor
|
||||||
const lastPoint = points[points.length - 1];
|
// always tracks the pointer regardless of tool activation/deactivation state.
|
||||||
|
useFrame((state) => {
|
||||||
let cursorPt: [number, number] | null = null;
|
|
||||||
if (lastPoint) {
|
|
||||||
cursorPt = calculateSnapPoint(lastPoint, cursorPosition);
|
|
||||||
} else if (points.length === 0) {
|
|
||||||
cursorPt = cursorPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current });
|
|
||||||
updateLines();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
|
||||||
if (!cursorRef.current) return;
|
if (!cursorRef.current) return;
|
||||||
|
|
||||||
// Snap to 0.5 grid
|
// Sync level Y from scene registry each frame
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2;
|
levelYRef.current = getLevelY();
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2;
|
|
||||||
cursorPosition = [gridX, gridZ];
|
|
||||||
levelYRef.current = event.position[1];
|
|
||||||
|
|
||||||
const ceilingY = event.position[1] + CEILING_HEIGHT;
|
// Raycast from camera through pointer onto horizontal plane at level Y
|
||||||
const gridY = event.position[1] + GRID_OFFSET;
|
raycaster.setFromCamera(state.pointer, state.camera);
|
||||||
|
levelPlane.constant = -levelYRef.current;
|
||||||
|
const hit = raycaster.ray.intersectPlane(levelPlane, hitPoint);
|
||||||
|
if (!hit) return;
|
||||||
|
|
||||||
// If we have points, snap to axis from last point
|
// Snap to 0.5m grid
|
||||||
|
const gridX = Math.round(hit.x * 2) / 2;
|
||||||
|
const gridZ = Math.round(hit.z * 2) / 2;
|
||||||
|
cursorPositionRef.current = [gridX, gridZ];
|
||||||
|
|
||||||
|
const ceilingY = levelYRef.current + CEILING_HEIGHT;
|
||||||
|
const gridY = levelYRef.current + GRID_OFFSET;
|
||||||
|
|
||||||
|
// Apply axis snapping from last placed point
|
||||||
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
||||||
|
let displayPoint: [number, number];
|
||||||
if (lastPoint) {
|
if (lastPoint) {
|
||||||
const snapped = calculateSnapPoint(lastPoint, cursorPosition);
|
displayPoint = calculateSnapPoint(lastPoint, cursorPositionRef.current);
|
||||||
cursorRef.current.position.set(snapped[0], ceilingY, snapped[1]);
|
|
||||||
// Also update grid-level cursor
|
|
||||||
if (gridCursorRef.current) {
|
|
||||||
gridCursorRef.current.position.set(snapped[0], gridY, snapped[1]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cursorRef.current.position.set(gridX, ceilingY, gridZ);
|
displayPoint = [gridX, gridZ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cursor mesh positions imperatively
|
||||||
|
cursorRef.current.position.set(displayPoint[0], ceilingY, displayPoint[1]);
|
||||||
if (gridCursorRef.current) {
|
if (gridCursorRef.current) {
|
||||||
gridCursorRef.current.position.set(gridX, gridY, gridZ);
|
gridCursorRef.current.position.set(displayPoint[0], gridY, displayPoint[1]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreview();
|
// Update line geometries imperatively
|
||||||
};
|
updateLines();
|
||||||
|
|
||||||
const onGridClick = (event: GridEvent) => {
|
// Only trigger React state update when snapped display position changes
|
||||||
|
if (
|
||||||
|
!lastDisplayRef.current ||
|
||||||
|
lastDisplayRef.current.x !== displayPoint[0] ||
|
||||||
|
lastDisplayRef.current.z !== displayPoint[1]
|
||||||
|
) {
|
||||||
|
lastDisplayRef.current = { x: displayPoint[0], z: displayPoint[1] };
|
||||||
|
setPreview({
|
||||||
|
points: [...pointsRef.current],
|
||||||
|
cursorPoint: displayPoint,
|
||||||
|
levelY: levelYRef.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click and double-click handlers for point placement
|
||||||
|
useEffect(() => {
|
||||||
if (!currentLevelId) return;
|
if (!currentLevelId) return;
|
||||||
|
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2;
|
// Initialize line geometries
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2;
|
if (mainLineRef.current) {
|
||||||
let clickPoint: [number, number] = [gridX, gridZ];
|
mainLineRef.current.geometry = new BufferGeometry();
|
||||||
|
}
|
||||||
|
if (closingLineRef.current) {
|
||||||
|
closingLineRef.current.geometry = new BufferGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state on level change
|
||||||
|
pointsRef.current = [];
|
||||||
|
lastDisplayRef.current = null;
|
||||||
|
setPreview({ points: [], cursorPoint: null, levelY: getLevelY() });
|
||||||
|
|
||||||
|
const onGridClick = (_event: GridEvent) => {
|
||||||
|
if (!currentLevelId) return;
|
||||||
|
|
||||||
|
// Use the cursor position tracked by useFrame (matches what user sees)
|
||||||
|
let clickPoint: [number, number] = [...cursorPositionRef.current];
|
||||||
|
|
||||||
// Snap to axis from last point
|
// Snap to axis from last point
|
||||||
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
||||||
@@ -230,16 +275,17 @@ export const CeilingTool: React.FC = () => {
|
|||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
pointsRef.current = [];
|
pointsRef.current = [];
|
||||||
|
lastDisplayRef.current = null;
|
||||||
setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
|
setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
|
||||||
mainLineRef.current.visible = false;
|
if (mainLineRef.current) mainLineRef.current.visible = false;
|
||||||
closingLineRef.current.visible = false;
|
if (closingLineRef.current) closingLineRef.current.visible = false;
|
||||||
|
|
||||||
// Deactivate tool
|
// Deactivate tool
|
||||||
setTool(null);
|
setTool(null);
|
||||||
} else {
|
} else {
|
||||||
// Add point to polygon
|
// Add point to polygon
|
||||||
pointsRef.current = [...pointsRef.current, clickPoint];
|
pointsRef.current = [...pointsRef.current, clickPoint];
|
||||||
updatePreview();
|
lastDisplayRef.current = null; // Force preview update on next frame
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -252,22 +298,20 @@ export const CeilingTool: React.FC = () => {
|
|||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
pointsRef.current = [];
|
pointsRef.current = [];
|
||||||
|
lastDisplayRef.current = null;
|
||||||
setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
|
setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
|
||||||
mainLineRef.current.visible = false;
|
if (mainLineRef.current) mainLineRef.current.visible = false;
|
||||||
closingLineRef.current.visible = false;
|
if (closingLineRef.current) closingLineRef.current.visible = false;
|
||||||
|
|
||||||
// Deactivate tool
|
// Deactivate tool
|
||||||
setTool(null);
|
setTool(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subscribe to events
|
|
||||||
emitter.on("grid:move", onGridMove);
|
|
||||||
emitter.on("grid:click", onGridClick);
|
emitter.on("grid:click", onGridClick);
|
||||||
emitter.on("grid:double-click", onGridDoubleClick);
|
emitter.on("grid:double-click", onGridDoubleClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off("grid:move", onGridMove);
|
|
||||||
emitter.off("grid:click", onGridClick);
|
emitter.off("grid:click", onGridClick);
|
||||||
emitter.off("grid:double-click", onGridDoubleClick);
|
emitter.off("grid:double-click", onGridDoubleClick);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { emitter, type GridEvent, useScene, SlabNode, type LevelNode } from "@pascal-app/core";
|
import { emitter, type GridEvent, useScene, SlabNode, type LevelNode, sceneRegistry } from "@pascal-app/core";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
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, Plane, Raycaster, Shape, Vector3 } from "three";
|
||||||
import useEditor from "@/store/use-editor";
|
import useEditor from "@/store/use-editor";
|
||||||
|
|
||||||
const Y_OFFSET = 0.02; // Small offset above floor level
|
const Y_OFFSET = 0.02; // Small offset above floor level
|
||||||
|
const UP = new Vector3(0, 1, 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -86,11 +88,18 @@ 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 levelYRef = useRef(0);
|
||||||
|
const cursorPositionRef = useRef<[number, number]>([0, 0]);
|
||||||
|
const lastDisplayRef = useRef<{ x: number; z: number } | null>(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);
|
||||||
|
|
||||||
|
// Reusable objects for raycasting (created once, avoid per-frame allocations)
|
||||||
|
const raycaster = useMemo(() => new Raycaster(), []);
|
||||||
|
const levelPlane = useMemo(() => new Plane(UP, 0), []);
|
||||||
|
const hitPoint = useMemo(() => new Vector3(), []);
|
||||||
|
|
||||||
// Preview state for reactive rendering (for shape and point markers)
|
// Preview state for reactive rendering (for shape and point markers)
|
||||||
const [preview, setPreview] = useState<PreviewState>({
|
const [preview, setPreview] = useState<PreviewState>({
|
||||||
points: [],
|
points: [],
|
||||||
@@ -98,17 +107,27 @@ export const SlabTool: React.FC = () => {
|
|||||||
levelY: 0,
|
levelY: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Resolve the current level's Y position from scene registry or node data
|
||||||
if (!currentLevelId) return;
|
const getLevelY = (): number => {
|
||||||
|
if (!currentLevelId) return 0;
|
||||||
let cursorPosition: [number, number] = [0, 0];
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId);
|
||||||
|
if (levelMesh) return levelMesh.position.y;
|
||||||
// Initialize line geometries
|
const levelNode = useScene.getState().nodes[currentLevelId];
|
||||||
mainLineRef.current.geometry = new BufferGeometry();
|
if (levelNode && 'level' in levelNode) {
|
||||||
closingLineRef.current.geometry = new BufferGeometry();
|
const levelMode = useViewer.getState().levelMode;
|
||||||
|
const LEVEL_HEIGHT = 2.5;
|
||||||
|
const EXPLODED_GAP = 5;
|
||||||
|
return ((levelNode as any).level || 0) * (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Imperatively update line geometries (called from useFrame)
|
||||||
const updateLines = () => {
|
const updateLines = () => {
|
||||||
|
if (!mainLineRef.current || !closingLineRef.current) return;
|
||||||
|
|
||||||
const points = pointsRef.current;
|
const points = pointsRef.current;
|
||||||
|
const cursorPosition = cursorPositionRef.current;
|
||||||
const y = levelYRef.current + Y_OFFSET;
|
const y = levelYRef.current + Y_OFFSET;
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
@@ -158,48 +177,78 @@ export const SlabTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePreview = () => {
|
// Per-frame cursor positioning via direct raycasting onto the level plane.
|
||||||
const points = pointsRef.current;
|
// This bypasses R3F's event propagation (grid:move), ensuring the cursor
|
||||||
const lastPoint = points[points.length - 1];
|
// always tracks the pointer regardless of tool activation/deactivation state.
|
||||||
|
useFrame((state) => {
|
||||||
let cursorPt: [number, number] | null = null;
|
|
||||||
if (lastPoint) {
|
|
||||||
cursorPt = calculateSnapPoint(lastPoint, cursorPosition);
|
|
||||||
} else if (points.length === 0) {
|
|
||||||
cursorPt = cursorPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current });
|
|
||||||
updateLines();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
|
||||||
if (!cursorRef.current) return;
|
if (!cursorRef.current) return;
|
||||||
|
|
||||||
// Snap to 0.5 grid
|
// Sync level Y from scene registry each frame
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2;
|
levelYRef.current = getLevelY();
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2;
|
|
||||||
cursorPosition = [gridX, gridZ];
|
|
||||||
levelYRef.current = event.position[1];
|
|
||||||
|
|
||||||
// If we have points, snap to axis from last point
|
// Raycast from camera through pointer onto horizontal plane at level Y
|
||||||
|
raycaster.setFromCamera(state.pointer, state.camera);
|
||||||
|
levelPlane.constant = -levelYRef.current;
|
||||||
|
const hit = raycaster.ray.intersectPlane(levelPlane, hitPoint);
|
||||||
|
if (!hit) return;
|
||||||
|
|
||||||
|
// Snap to 0.5m grid
|
||||||
|
const gridX = Math.round(hit.x * 2) / 2;
|
||||||
|
const gridZ = Math.round(hit.z * 2) / 2;
|
||||||
|
cursorPositionRef.current = [gridX, gridZ];
|
||||||
|
|
||||||
|
// Apply axis snapping from last placed point
|
||||||
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
||||||
|
let displayPoint: [number, number];
|
||||||
if (lastPoint) {
|
if (lastPoint) {
|
||||||
const snapped = calculateSnapPoint(lastPoint, cursorPosition);
|
displayPoint = calculateSnapPoint(lastPoint, cursorPositionRef.current);
|
||||||
cursorRef.current.position.set(snapped[0], event.position[1], snapped[1]);
|
|
||||||
} else {
|
} else {
|
||||||
cursorRef.current.position.set(gridX, event.position[1], gridZ);
|
displayPoint = [gridX, gridZ];
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreview();
|
// Update cursor mesh position imperatively
|
||||||
};
|
cursorRef.current.position.set(displayPoint[0], levelYRef.current, displayPoint[1]);
|
||||||
|
|
||||||
const onGridClick = (event: GridEvent) => {
|
// Update line geometries imperatively
|
||||||
|
updateLines();
|
||||||
|
|
||||||
|
// Only trigger React state update when snapped display position changes
|
||||||
|
if (
|
||||||
|
!lastDisplayRef.current ||
|
||||||
|
lastDisplayRef.current.x !== displayPoint[0] ||
|
||||||
|
lastDisplayRef.current.z !== displayPoint[1]
|
||||||
|
) {
|
||||||
|
lastDisplayRef.current = { x: displayPoint[0], z: displayPoint[1] };
|
||||||
|
setPreview({
|
||||||
|
points: [...pointsRef.current],
|
||||||
|
cursorPoint: displayPoint,
|
||||||
|
levelY: levelYRef.current,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click and double-click handlers for point placement
|
||||||
|
useEffect(() => {
|
||||||
if (!currentLevelId) return;
|
if (!currentLevelId) return;
|
||||||
|
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2;
|
// Initialize line geometries
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2;
|
if (mainLineRef.current) {
|
||||||
let clickPoint: [number, number] = [gridX, gridZ];
|
mainLineRef.current.geometry = new BufferGeometry();
|
||||||
|
}
|
||||||
|
if (closingLineRef.current) {
|
||||||
|
closingLineRef.current.geometry = new BufferGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state on level change
|
||||||
|
pointsRef.current = [];
|
||||||
|
lastDisplayRef.current = null;
|
||||||
|
setPreview({ points: [], cursorPoint: null, levelY: getLevelY() });
|
||||||
|
|
||||||
|
const onGridClick = (_event: GridEvent) => {
|
||||||
|
if (!currentLevelId) return;
|
||||||
|
|
||||||
|
// Use the cursor position tracked by useFrame (matches what user sees)
|
||||||
|
let clickPoint: [number, number] = [...cursorPositionRef.current];
|
||||||
|
|
||||||
// Snap to axis from last point
|
// Snap to axis from last point
|
||||||
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1];
|
||||||
@@ -221,13 +270,14 @@ export const SlabTool: React.FC = () => {
|
|||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
pointsRef.current = [];
|
pointsRef.current = [];
|
||||||
setPreview({ points: [], cursorPoint: null, levelY: 0 });
|
lastDisplayRef.current = null;
|
||||||
mainLineRef.current.visible = false;
|
setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
|
||||||
closingLineRef.current.visible = false;
|
if (mainLineRef.current) mainLineRef.current.visible = false;
|
||||||
|
if (closingLineRef.current) closingLineRef.current.visible = false;
|
||||||
} else {
|
} else {
|
||||||
// Add point to polygon
|
// Add point to polygon
|
||||||
pointsRef.current = [...pointsRef.current, clickPoint];
|
pointsRef.current = [...pointsRef.current, clickPoint];
|
||||||
updatePreview();
|
lastDisplayRef.current = null; // Force preview update on next frame
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -242,19 +292,17 @@ export const SlabTool: React.FC = () => {
|
|||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
pointsRef.current = [];
|
pointsRef.current = [];
|
||||||
setPreview({ points: [], cursorPoint: null, levelY: 0 });
|
lastDisplayRef.current = null;
|
||||||
mainLineRef.current.visible = false;
|
setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current });
|
||||||
closingLineRef.current.visible = false;
|
if (mainLineRef.current) mainLineRef.current.visible = false;
|
||||||
|
if (closingLineRef.current) closingLineRef.current.visible = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Subscribe to events
|
|
||||||
emitter.on("grid:move", onGridMove);
|
|
||||||
emitter.on("grid:click", onGridClick);
|
emitter.on("grid:click", onGridClick);
|
||||||
emitter.on("grid:double-click", onGridDoubleClick);
|
emitter.on("grid:double-click", onGridDoubleClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off("grid:move", onGridMove);
|
|
||||||
emitter.off("grid:click", onGridClick);
|
emitter.off("grid:click", onGridClick);
|
||||||
emitter.off("grid:double-click", onGridDoubleClick);
|
emitter.off("grid:double-click", onGridDoubleClick);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user