import { emitter, type GridEvent, useScene, RoofNode, type LevelNode, type AnyNode } from "@pascal-app/core"; import { useViewer } from "@pascal-app/viewer"; import { useEffect, useMemo, useRef, useState } from "react"; import { BufferGeometry, DoubleSide, type Line, Vector3 } from "three"; import useEditor from "@/store/use-editor"; // Default roof dimensions const DEFAULT_HEIGHT = 1.5; const PREVIEW_LINE_HEIGHT = 0.03; // Very thin preview /** * Creates a roof with the given corners */ const commitRoofPlacement = ( levelId: LevelNode["id"], corner1: [number, number, number], corner2: [number, number, number] ): RoofNode["id"] => { const { createNode, nodes } = useScene.getState(); // Calculate center position and dimensions from corners const centerX = (corner1[0] + corner2[0]) / 2; const centerZ = (corner1[2] + corner2[2]) / 2; const length = Math.abs(corner2[0] - corner1[0]); const width = Math.abs(corner2[2] - corner1[2]); // Split width evenly between left and right slopes const slopeWidth = Math.max(width / 2, 0.5); // Count existing roofs for naming const roofCount = Object.values(nodes).filter((n) => n.type === "roof").length; const name = `Roof ${roofCount + 1}`; const roof = RoofNode.parse({ name, position: [centerX, 0, centerZ], // Y is always 0 length: Math.max(length, 0.5), height: DEFAULT_HEIGHT, leftWidth: slopeWidth, rightWidth: slopeWidth, }); createNode(roof, levelId); return roof.id; }; type PreviewState = { corner1: [number, number, number] | null; cursorPosition: [number, number, number]; levelY: number; }; export const RoofTool: React.FC = () => { const outlineRef = useRef(null!); const currentLevelId = useViewer((state) => state.selection.levelId); const setSelection = useViewer((state) => state.setSelection); const setTool = useEditor((state) => state.setTool); const setMode = useEditor((state) => state.setMode); const corner1Ref = useRef<[number, number, number] | null>(null); const [preview, setPreview] = useState({ corner1: null, cursorPosition: [0, 0, 0], levelY: 0, }); useEffect(() => { if (!currentLevelId) return; // Initialize outline geometry outlineRef.current.geometry = new BufferGeometry(); const updateOutline = (corner1: [number, number, number], corner2: [number, number, number]) => { const y = corner1[1] + PREVIEW_LINE_HEIGHT; const points = [ new Vector3(corner1[0], y, corner1[2]), new Vector3(corner2[0], y, corner1[2]), new Vector3(corner2[0], y, corner2[2]), new Vector3(corner1[0], y, corner2[2]), new Vector3(corner1[0], y, corner1[2]), // Close the loop ]; outlineRef.current.geometry.dispose(); outlineRef.current.geometry = new BufferGeometry().setFromPoints(points); outlineRef.current.visible = true; }; const onGridMove = (event: GridEvent) => { // Snap to 0.5 grid const gridX = Math.round(event.position[0] * 2) / 2; const gridZ = Math.round(event.position[2] * 2) / 2; const y = event.position[1]; const cursorPosition: [number, number, number] = [gridX, y, gridZ]; setPreview({ corner1: corner1Ref.current, cursorPosition, levelY: y, }); // Update outline if we have first corner if (corner1Ref.current) { updateOutline(corner1Ref.current, cursorPosition); } }; 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; const y = event.position[1]; if (!corner1Ref.current) { // First click - set corner 1 corner1Ref.current = [gridX, y, gridZ]; setPreview((prev) => ({ ...prev, corner1: corner1Ref.current, })); } else { // Second click - create the roof const roofId = commitRoofPlacement( currentLevelId, corner1Ref.current, [gridX, y, gridZ] ); // Auto-select the newly created roof setSelection({ selectedIds: [roofId as AnyNode["id"]] }); // Reset state corner1Ref.current = null; outlineRef.current.visible = false; // Switch to select mode and deactivate tool setMode('select'); setTool(null); } }; // Subscribe to events emitter.on("grid:move", onGridMove); emitter.on("grid:click", onGridClick); return () => { emitter.off("grid:move", onGridMove); emitter.off("grid:click", onGridClick); // Reset state on unmount corner1Ref.current = null; }; }, [currentLevelId, setTool, setSelection, setMode]); const { corner1, cursorPosition, levelY } = preview; // Calculate preview dimensions for display const previewDimensions = useMemo(() => { if (!corner1) return null; const length = Math.abs(cursorPosition[0] - corner1[0]); const width = Math.abs(cursorPosition[2] - corner1[2]); const centerX = (corner1[0] + cursorPosition[0]) / 2; const centerZ = (corner1[2] + cursorPosition[2]) / 2; return { length, width, centerX, centerZ }; }, [corner1, cursorPosition]); return ( {/* Outline showing rectangle being drawn */} {/* @ts-ignore */} {/* First corner marker */} {corner1 && ( )} {/* Cursor marker on ground */} {/* Thin preview fill when drawing */} {previewDimensions && previewDimensions.length > 0.1 && previewDimensions.width > 0.1 && ( )} ); };