조사표 8~13절 검토에서 "기본 기능"으로 고른 것을 반영한다. - 좌표 입력: 절대·상대 좌표가 양수만 받아 `@-100,50`을 거부했다. 부호를 허용하고 극좌표 `@거리<각도`·`거리<각도`를 더했다. `-`·`+`가 확대·축소 단축키로 먼저 잡혀 음수의 첫 글자를 먹고 있어 그 두 단축키를 뺐다(줌은 휠·뷰 막대·명령). - 그립 편집: 선택 객체에 그립을 그리고 집어서 옮긴다. 선 끝점·중점, 폴리선 정점, 사각형 모서리, 원 중심·반지름, 문자·점 기준점. 폴리선은 세그먼트 중점을 끌면 정점이 늘고 정점 위 Ctrl+클릭이면 준다. 형상 필드가 private이라 공개 생성자로 다시 만들어 바꿔 끼우고 id·도면층·색·그룹을 물려받는다. - 자동 백업·복구: 5초 디바운스로 복구 전용 키에 저장하고, 시작할 때 백업이 있으면 눌러서 되살리는 안내를 띄운다. 저장(QSAVE)에 성공하면 백업을 지운다. - 선택 순환: 같은 자리를 다시 클릭하면 겹친 후보를 차례로 돌린다. - 상태막대: `극좌표 추적` 버튼이 `직교`와 같은 onClick이라 같은 일을 하고 있었다. 각각 45°·90°를 켜고 끄도록 고치고, 스냅 추적 토글과 F3·F7·F8·F10을 붙였다. - 문자 굵게·기울임을 캔버스·SVG·JSON·스타일 패널에 연결했다. - 조사표: 이미 되어 있던 5건의 표기를 정정하고, 출력·내보내기(9절)는 결재창 이후 PDF·DXF·DWG로 반영할 것이라 보류(P)로 구분해 사유를 남겼다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
181 lines
5.8 KiB
TypeScript
181 lines
5.8 KiB
TypeScript
import { Point } from '@flatten-js/core';
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom/client';
|
|
import { Actor, type MachineSnapshot } from 'xstate';
|
|
import { HIGHLIGHT_ENTITY_DISTANCE, 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 { 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 {
|
|
getActiveToolActor,
|
|
getCanvas,
|
|
getHoveredSnapPoints,
|
|
getLastDrawTimestamp,
|
|
getScreenCanvasDrawController,
|
|
getSnapPoint,
|
|
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(
|
|
<React.StrictMode>
|
|
<App />
|
|
</React.StrictMode>
|
|
);
|
|
|
|
// 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: <explanation>
|
|
const activeToolSnapshot: MachineSnapshot<any, any, any, any, any, any, any, any> | 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) {
|
|
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,
|
|
HIGHLIGHT_ENTITY_DISTANCE
|
|
)
|
|
);
|
|
|
|
if (distance < HIGHLIGHT_ENTITY_DISTANCE) {
|
|
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();
|
|
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();
|
|
});
|