import './random-uuid-polyfill'; import { Point } from '@flatten-js/core'; import React from 'react'; import ReactDOM from 'react-dom/client'; import { Actor, type MachineSnapshot } from 'xstate'; import { SNAP_POINT_DISTANCE } from './App.consts'; import App from './App.tsx'; import { TOOL_STATE_MACHINES } from './commands/registry'; import { ScreenCanvasDrawController } from './drawControllers/screenCanvas.drawController'; import { registerAutoSave } from './helpers/autosave.ts'; import { registerCadDebugHook } from './helpers/debug-hook.ts'; import { draw } from './helpers/draw'; import { findClosestEntity } from './helpers/find-closest-entity'; import { getNewLayer } from './helpers/get-new-layer.ts'; import { pickRadius } from './helpers/pick-radius'; import { scenePerf } from './helpers/scene-cache'; import { queryEntitiesNearPoint } from './helpers/spatial-index'; import { trackHoveredSnapPoint } from './helpers/track-hovered-snap-points'; import { InputController } from './inputController/input-controller.ts'; import { registerAisloDrawingBridge } from './integration/aislo-drawing-bridge.ts'; import { registerDxfClipPaste } from './integration/dxf-clip-paste.ts'; import { getActiveToolActor, getCanvas, getHoveredSnapPoints, getLastDrawTimestamp, getScreenCanvasDrawController, getSnapPoint, isViewOnly, setActiveLayerId, setActiveToolActor, setCanvas, setEntities, setHighlightedEntityIds, setHoveredSnapPoints, setInputController, setLastDrawTimestamp, setLayers, setScreenCanvasDrawController, } from './state'; import { syncThemeFromHost } from './theme.ts'; import { Tool } from './tools'; import { ActorEvent, type DrawEvent } from './tools/tool.types'; // 호스트 앱의 화이트/블랙 모드를 먼저 붙인 뒤 렌더한다. syncThemeFromHost(); ReactDOM.createRoot(document.getElementById('root') as HTMLDivElement).render( ); // Hover highlight throttle: the closest-entity scan is O(entities) so it runs // at most every HOVER_THROTTLE_MS and only when the mouse actually moved. const HOVER_THROTTLE_MS = 30; let lastHoverCheckAt = 0; let lastHoverMouseX = Number.NaN; let lastHoverMouseY = Number.NaN; function startDrawLoop( screenCanvasDrawController: ScreenCanvasDrawController, timestamp: DOMHighResTimeStamp ) { const lastDrawTimestamp = getLastDrawTimestamp(); const elapsedTime = timestamp - lastDrawTimestamp; setLastDrawTimestamp(timestamp); scenePerf.avgFrameMs = scenePerf.avgFrameMs * 0.9 + elapsedTime * 0.1; // biome-ignore lint/suspicious/noExplicitAny: const activeToolSnapshot: MachineSnapshot | undefined = getActiveToolActor()?.getSnapshot(); if ( activeToolSnapshot?.status === 'active' && activeToolSnapshot?.can({ type: ActorEvent.DRAW }) ) { getActiveToolActor()?.send({ type: ActorEvent.DRAW, drawController: screenCanvasDrawController, } as DrawEvent); } /** * Highlight the entity closest to the mouse when the select tool is active */ if (getActiveToolActor()?.getSnapshot()?.context?.type === Tool.SELECT && !isViewOnly()) { const screenCanvasDrawController = getScreenCanvasDrawController(); if (!screenCanvasDrawController) { throw new Error('getScreenCanvasDrawController() returned null'); } const mouseLocation = screenCanvasDrawController.getScreenMouseLocation(); const mouseMoved = mouseLocation.x !== lastHoverMouseX || mouseLocation.y !== lastHoverMouseY; if (mouseMoved && timestamp - lastHoverCheckAt >= HOVER_THROTTLE_MS) { lastHoverCheckAt = timestamp; lastHoverMouseX = mouseLocation.x; lastHoverMouseY = mouseLocation.y; const worldMouseLocation = screenCanvasDrawController.getWorldMouseLocation(); const { distance, entity: closestEntity } = findClosestEntity( worldMouseLocation, // 공간 인덱스로 후보를 좁혀 O(전체) 스캔 제거 queryEntitiesNearPoint(worldMouseLocation.x, worldMouseLocation.y, pickRadius()) ); if (distance < pickRadius()) { setHighlightedEntityIds([closestEntity.id]); } } } /** * Track hovered snap points */ trackHoveredSnapPoint( getSnapPoint(), getHoveredSnapPoints(), setHoveredSnapPoints, SNAP_POINT_DISTANCE / screenCanvasDrawController.getScreenScale(), elapsedTime ); /** * Draw everything on the canvas */ draw(screenCanvasDrawController); requestAnimationFrame((newTimestamp: DOMHighResTimeStamp) => { startDrawLoop(screenCanvasDrawController, newTimestamp); }); } function handleWindowResize() { const canvas = getCanvas(); if (canvas) { const bounds = canvas.getBoundingClientRect(); const width = Math.max(1, Math.round(bounds.width)); const height = Math.max(1, Math.round(bounds.height)); canvas.width = width; canvas.height = height; getScreenCanvasDrawController().setCanvasSize(new Point(width, height)); } } function initApplication() { const canvas = document.getElementsByTagName('canvas')[0] as HTMLCanvasElement | null; if (canvas) { setCanvas(canvas); const context = canvas.getContext('2d'); if (!context) return; setEntities([], true); // Creates the first undo entry const layers = [getNewLayer()]; setLayers(layers); setActiveLayerId(layers[0].id); registerAisloDrawingBridge(); registerDxfClipPaste(); registerCadDebugHook(); registerAutoSave(); const screenCanvasDrawController = new ScreenCanvasDrawController(context); setScreenCanvasDrawController(screenCanvasDrawController); window.addEventListener('resize', handleWindowResize); new ResizeObserver(handleWindowResize).observe(canvas); const inputController = new InputController(); setInputController(inputController); handleWindowResize(); startDrawLoop(screenCanvasDrawController, 0); const lineToolActor = new Actor(TOOL_STATE_MACHINES[Tool.LINE]); lineToolActor.start(); setActiveToolActor(lineToolActor); } } document.addEventListener('DOMContentLoaded', () => { initApplication(); });