diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index c96a64e4..0d8a8000 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -148,6 +148,15 @@ const Grid = ({ const levelMesh = sceneRegistry.nodes.get(currentLevelId) if (levelMesh) { 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) diff --git a/apps/editor/components/tools/ceiling/ceiling-tool.tsx b/apps/editor/components/tools/ceiling/ceiling-tool.tsx index 1abcbcd2..d01c0850 100644 --- a/apps/editor/components/tools/ceiling/ceiling-tool.tsx +++ b/apps/editor/components/tools/ceiling/ceiling-tool.tsx @@ -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 { useFrame } from "@react-three/fiber"; 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"; const CEILING_HEIGHT = 2.52; // Slightly above default ceiling height 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 @@ -87,10 +89,17 @@ export const CeilingTool: React.FC = () => { const mainLineRef = useRef(null!); const closingLineRef = useRef(null!); const pointsRef = useRef>([]); - 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 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) const [preview, setPreview] = useState({ points: [], @@ -98,118 +107,154 @@ export const CeilingTool: React.FC = () => { levelY: 0, }); + // Resolve the current level's Y position from scene registry or node data + const getLevelY = (): number => { + if (!currentLevelId) return 0; + const levelMesh = sceneRegistry.nodes.get(currentLevelId); + if (levelMesh) return levelMesh.position.y; + 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; + return ((levelNode as any).level || 0) * (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0)); + } + return 0; + }; + + // Imperatively update line geometries (called from useFrame) + const updateLines = () => { + if (!mainLineRef.current || !closingLineRef.current) return; + + const points = pointsRef.current; + const cursorPosition = cursorPositionRef.current; + const ceilingY = levelYRef.current + CEILING_HEIGHT; + + if (points.length === 0) { + mainLineRef.current.visible = false; + closingLineRef.current.visible = false; + return; + } + + // Build main line points + const linePoints: Vector3[] = points.map( + ([x, z]) => new Vector3(x, ceilingY, z) + ); + + // Add cursor point + const lastPoint = points[points.length - 1]; + if (lastPoint) { + const snapped = calculateSnapPoint(lastPoint, cursorPosition); + if (isValidPoint(snapped)) { + linePoints.push(new Vector3(snapped[0], ceilingY, snapped[1])); + } + } + + // Update main line geometry + if (linePoints.length >= 2) { + mainLineRef.current.geometry.dispose(); + mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints); + mainLineRef.current.visible = true; + } else { + mainLineRef.current.visible = false; + } + + // Update closing line (from cursor back to first point) + const firstPoint = points[0]; + if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) { + const snapped = calculateSnapPoint(lastPoint, cursorPosition); + if (isValidPoint(snapped)) { + const closingPoints = [ + new Vector3(snapped[0], ceilingY, snapped[1]), + new Vector3(firstPoint[0], ceilingY, firstPoint[1]), + ]; + closingLineRef.current.geometry.dispose(); + closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); + closingLineRef.current.visible = true; + } + } else { + closingLineRef.current.visible = false; + } + }; + + // Per-frame cursor positioning via direct raycasting onto the level plane. + // This bypasses R3F's event propagation (grid:move), ensuring the cursor + // always tracks the pointer regardless of tool activation/deactivation state. + useFrame((state) => { + if (!cursorRef.current) return; + + // Sync level Y from scene registry each frame + levelYRef.current = getLevelY(); + + // 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]; + + 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]; + let displayPoint: [number, number]; + if (lastPoint) { + displayPoint = calculateSnapPoint(lastPoint, cursorPositionRef.current); + } else { + displayPoint = [gridX, gridZ]; + } + + // Update cursor mesh positions imperatively + cursorRef.current.position.set(displayPoint[0], ceilingY, displayPoint[1]); + if (gridCursorRef.current) { + gridCursorRef.current.position.set(displayPoint[0], gridY, displayPoint[1]); + } + + // 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; - let cursorPosition: [number, number] = [0, 0]; - // Initialize line geometries - mainLineRef.current.geometry = new BufferGeometry(); - closingLineRef.current.geometry = new BufferGeometry(); + if (mainLineRef.current) { + mainLineRef.current.geometry = new BufferGeometry(); + } + if (closingLineRef.current) { + closingLineRef.current.geometry = new BufferGeometry(); + } - const updateLines = () => { - const points = pointsRef.current; - const ceilingY = levelYRef.current + CEILING_HEIGHT; + // Reset state on level change + pointsRef.current = []; + lastDisplayRef.current = null; + setPreview({ points: [], cursorPoint: null, levelY: getLevelY() }); - if (points.length === 0) { - mainLineRef.current.visible = false; - closingLineRef.current.visible = false; - return; - } - - // Build main line points - const linePoints: Vector3[] = points.map( - ([x, z]) => new Vector3(x, ceilingY, z) - ); - - // Add cursor point - const lastPoint = points[points.length - 1]; - if (lastPoint) { - const snapped = calculateSnapPoint(lastPoint, cursorPosition); - if (isValidPoint(snapped)) { - linePoints.push(new Vector3(snapped[0], ceilingY, snapped[1])); - } - } - - // Update main line geometry - if (linePoints.length >= 2) { - mainLineRef.current.geometry.dispose(); - mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints); - mainLineRef.current.visible = true; - } else { - mainLineRef.current.visible = false; - } - - // Update closing line (from cursor back to first point) - const firstPoint = points[0]; - if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) { - const snapped = calculateSnapPoint(lastPoint, cursorPosition); - if (isValidPoint(snapped)) { - const closingPoints = [ - new Vector3(snapped[0], ceilingY, snapped[1]), - new Vector3(firstPoint[0], ceilingY, firstPoint[1]), - ]; - closingLineRef.current.geometry.dispose(); - closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); - closingLineRef.current.visible = true; - } - } else { - closingLineRef.current.visible = false; - } - }; - - const updatePreview = () => { - const points = pointsRef.current; - const lastPoint = points[points.length - 1]; - - 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; - - // Snap to 0.5 grid - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - 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 - const lastPoint = pointsRef.current[pointsRef.current.length - 1]; - if (lastPoint) { - const snapped = calculateSnapPoint(lastPoint, cursorPosition); - 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 { - cursorRef.current.position.set(gridX, ceilingY, gridZ); - if (gridCursorRef.current) { - gridCursorRef.current.position.set(gridX, gridY, gridZ); - } - } - - updatePreview(); - }; - - const onGridClick = (event: GridEvent) => { + const onGridClick = (_event: GridEvent) => { if (!currentLevelId) return; - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - let clickPoint: [number, number] = [gridX, gridZ]; + // Use the cursor position tracked by useFrame (matches what user sees) + let clickPoint: [number, number] = [...cursorPositionRef.current]; // Snap to axis from last point const lastPoint = pointsRef.current[pointsRef.current.length - 1]; @@ -230,16 +275,17 @@ export const CeilingTool: React.FC = () => { // Reset state pointsRef.current = []; + lastDisplayRef.current = null; setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current }); - mainLineRef.current.visible = false; - closingLineRef.current.visible = false; + if (mainLineRef.current) mainLineRef.current.visible = false; + if (closingLineRef.current) closingLineRef.current.visible = false; // Deactivate tool setTool(null); } else { // Add point to polygon 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 pointsRef.current = []; + lastDisplayRef.current = null; setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current }); - mainLineRef.current.visible = false; - closingLineRef.current.visible = false; + if (mainLineRef.current) mainLineRef.current.visible = false; + if (closingLineRef.current) closingLineRef.current.visible = false; // Deactivate tool setTool(null); } }; - // Subscribe to events - emitter.on("grid:move", onGridMove); emitter.on("grid:click", onGridClick); emitter.on("grid:double-click", onGridDoubleClick); return () => { - emitter.off("grid:move", onGridMove); emitter.off("grid:click", onGridClick); emitter.off("grid:double-click", onGridDoubleClick); diff --git a/apps/editor/components/tools/slab/slab-tool.tsx b/apps/editor/components/tools/slab/slab-tool.tsx index 6440b302..b3db9672 100644 --- a/apps/editor/components/tools/slab/slab-tool.tsx +++ b/apps/editor/components/tools/slab/slab-tool.tsx @@ -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 { useFrame } from "@react-three/fiber"; 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"; 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 @@ -86,11 +88,18 @@ export const SlabTool: React.FC = () => { const mainLineRef = useRef(null!); const closingLineRef = useRef(null!); const pointsRef = useRef>([]); - 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 setSelection = useViewer((state) => state.setSelection); 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) const [preview, setPreview] = useState({ points: [], @@ -98,108 +107,148 @@ export const SlabTool: React.FC = () => { levelY: 0, }); + // Resolve the current level's Y position from scene registry or node data + const getLevelY = (): number => { + if (!currentLevelId) return 0; + const levelMesh = sceneRegistry.nodes.get(currentLevelId); + if (levelMesh) return levelMesh.position.y; + 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; + return ((levelNode as any).level || 0) * (LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP : 0)); + } + return 0; + }; + + // Imperatively update line geometries (called from useFrame) + const updateLines = () => { + if (!mainLineRef.current || !closingLineRef.current) return; + + const points = pointsRef.current; + const cursorPosition = cursorPositionRef.current; + const y = levelYRef.current + Y_OFFSET; + + if (points.length === 0) { + mainLineRef.current.visible = false; + closingLineRef.current.visible = false; + return; + } + + // Build main line points + const linePoints: Vector3[] = points.map( + ([x, z]) => new Vector3(x, y, z) + ); + + // Add cursor point + const lastPoint = points[points.length - 1]; + if (lastPoint) { + const snapped = calculateSnapPoint(lastPoint, cursorPosition); + if (isValidPoint(snapped)) { + linePoints.push(new Vector3(snapped[0], y, snapped[1])); + } + } + + // Update main line geometry + if (linePoints.length >= 2) { + mainLineRef.current.geometry.dispose(); + mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints); + mainLineRef.current.visible = true; + } else { + mainLineRef.current.visible = false; + } + + // Update closing line (from cursor back to first point) + const firstPoint = points[0]; + if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) { + const snapped = calculateSnapPoint(lastPoint, cursorPosition); + if (isValidPoint(snapped)) { + const closingPoints = [ + new Vector3(snapped[0], y, snapped[1]), + new Vector3(firstPoint[0], y, firstPoint[1]), + ]; + closingLineRef.current.geometry.dispose(); + closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); + closingLineRef.current.visible = true; + } + } else { + closingLineRef.current.visible = false; + } + }; + + // Per-frame cursor positioning via direct raycasting onto the level plane. + // This bypasses R3F's event propagation (grid:move), ensuring the cursor + // always tracks the pointer regardless of tool activation/deactivation state. + useFrame((state) => { + if (!cursorRef.current) return; + + // Sync level Y from scene registry each frame + levelYRef.current = getLevelY(); + + // 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]; + let displayPoint: [number, number]; + if (lastPoint) { + displayPoint = calculateSnapPoint(lastPoint, cursorPositionRef.current); + } else { + displayPoint = [gridX, gridZ]; + } + + // Update cursor mesh position imperatively + cursorRef.current.position.set(displayPoint[0], levelYRef.current, displayPoint[1]); + + // 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; - let cursorPosition: [number, number] = [0, 0]; - // Initialize line geometries - mainLineRef.current.geometry = new BufferGeometry(); - closingLineRef.current.geometry = new BufferGeometry(); + if (mainLineRef.current) { + mainLineRef.current.geometry = new BufferGeometry(); + } + if (closingLineRef.current) { + closingLineRef.current.geometry = new BufferGeometry(); + } - const updateLines = () => { - const points = pointsRef.current; - const y = levelYRef.current + Y_OFFSET; + // Reset state on level change + pointsRef.current = []; + lastDisplayRef.current = null; + setPreview({ points: [], cursorPoint: null, levelY: getLevelY() }); - if (points.length === 0) { - mainLineRef.current.visible = false; - closingLineRef.current.visible = false; - return; - } - - // Build main line points - const linePoints: Vector3[] = points.map( - ([x, z]) => new Vector3(x, y, z) - ); - - // Add cursor point - const lastPoint = points[points.length - 1]; - if (lastPoint) { - const snapped = calculateSnapPoint(lastPoint, cursorPosition); - if (isValidPoint(snapped)) { - linePoints.push(new Vector3(snapped[0], y, snapped[1])); - } - } - - // Update main line geometry - if (linePoints.length >= 2) { - mainLineRef.current.geometry.dispose(); - mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints); - mainLineRef.current.visible = true; - } else { - mainLineRef.current.visible = false; - } - - // Update closing line (from cursor back to first point) - const firstPoint = points[0]; - if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) { - const snapped = calculateSnapPoint(lastPoint, cursorPosition); - if (isValidPoint(snapped)) { - const closingPoints = [ - new Vector3(snapped[0], y, snapped[1]), - new Vector3(firstPoint[0], y, firstPoint[1]), - ]; - closingLineRef.current.geometry.dispose(); - closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints); - closingLineRef.current.visible = true; - } - } else { - closingLineRef.current.visible = false; - } - }; - - const updatePreview = () => { - const points = pointsRef.current; - const lastPoint = points[points.length - 1]; - - 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; - - // Snap to 0.5 grid - const gridX = Math.round(event.position[0] * 2) / 2; - 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 - const lastPoint = pointsRef.current[pointsRef.current.length - 1]; - if (lastPoint) { - const snapped = calculateSnapPoint(lastPoint, cursorPosition); - cursorRef.current.position.set(snapped[0], event.position[1], snapped[1]); - } else { - cursorRef.current.position.set(gridX, event.position[1], gridZ); - } - - updatePreview(); - }; - - const onGridClick = (event: GridEvent) => { + const onGridClick = (_event: GridEvent) => { if (!currentLevelId) return; - const gridX = Math.round(event.position[0] * 2) / 2; - const gridZ = Math.round(event.position[2] * 2) / 2; - let clickPoint: [number, number] = [gridX, gridZ]; + // Use the cursor position tracked by useFrame (matches what user sees) + let clickPoint: [number, number] = [...cursorPositionRef.current]; // Snap to axis from last point const lastPoint = pointsRef.current[pointsRef.current.length - 1]; @@ -221,13 +270,14 @@ export const SlabTool: React.FC = () => { // Reset state pointsRef.current = []; - setPreview({ points: [], cursorPoint: null, levelY: 0 }); - mainLineRef.current.visible = false; - closingLineRef.current.visible = false; + lastDisplayRef.current = null; + setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current }); + if (mainLineRef.current) mainLineRef.current.visible = false; + if (closingLineRef.current) closingLineRef.current.visible = false; } else { // Add point to polygon 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 pointsRef.current = []; - setPreview({ points: [], cursorPoint: null, levelY: 0 }); - mainLineRef.current.visible = false; - closingLineRef.current.visible = false; + lastDisplayRef.current = null; + setPreview({ points: [], cursorPoint: null, levelY: levelYRef.current }); + 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:double-click", onGridDoubleClick); return () => { - emitter.off("grid:move", onGridMove); emitter.off("grid:click", onGridClick); emitter.off("grid:double-click", onGridDoubleClick); diff --git a/bun.lock b/bun.lock index eba03d97..93c676af 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "editor",