Files
Aislo/B07_DesignDetail/openwebcad/src/main.tsx
T
eomsangdonandClaude Opus 5.5 de250ab759 refactor(cad): CAD 앱 겉 자체 변종을 부품 상자 부품으로 — 상태막대 켜기 칩 · 패널 탭 · 떠 있는 판 · 메뉴 · 아이콘 단추 · 확인창 (PLAN 553-6)
- 부품 상자 CSS · 단추 기본 꼴을 CAD iframe 에 실음(integration/ui-kit.ts) · 자리 · 폭은 cad-* 그대로
- 상태막대 = ui-toggle--chip 22 · 특성/레이어 = ui-tabs 36 · ↶↷ · 리본 접기 · 탐색 막대 = ui-icon-btn 20 · 32
- 빠른 특성 · 블록 라이브러리 · 자리표 = ui-float-panel · 리본 넘침 · 스냅 · 명령 자동완성 = ui-ctx-menu
- window.confirm · window.prompt → askConfirm · askText · 빈 안내 = ui-empty 작은 꼴
- 높이 단: 리본 칸 19→18 · 명령 칸 26→28 · 레이어 줄 40→42 · 문서 탭 25→24 · 도각 편집 단추 24
- 마우스 바탕 = --color-hover · 켜짐 = --color-accent-soft · 도면 그림 색(--draw-*) 안 건드림
- 안 쓰는 DropdownButton 지움(쓰는 곳 0 · grep)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 22:35:43 +09:00

190 lines
6.5 KiB
TypeScript

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';
// 부품 상자 CSS 를 앱 CSS 보다 먼저 실음 — 자리 · 폭은 뒤에 오는 App.css 의 `cad-*` 가 덮음
import './integration/ui-kit.ts';
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 { registerAisloTableExport } from './integration/aislo-table-export.ts';
import { registerDxfClipPaste } from './integration/dxf-clip-paste.ts';
import { registerHostBurgerPad } from './integration/host-burger.ts';
import {
getActiveToolActor,
getCanvas,
getHoveredSnapPoints,
getLastDrawTimestamp,
getScreenCanvasDrawController,
getSnapPoint,
isViewOnly,
setActiveLayerId,
setActiveToolActor,
setBare,
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();
// 부모가 `?bare` 로 띄우면 첫 그림부터 캔버스만 — 도면을 넘길 때 다시 정함(522-2)
if (new URLSearchParams(window.location.search).has('bare')) setBare(true);
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 && !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();
registerAisloTableExport(); // 고른 범위 → 면적산출 표(M02 83장)
registerDxfClipPaste();
registerHostBurgerPad(); // 앱 ☰ 자리 비키기(기기대응 7-2)
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);
// 오토캐드처럼 「명령:」 쉼(선택 도구)으로 시작
setActiveToolActor(new Actor(TOOL_STATE_MACHINES[Tool.SELECT]));
}
}
document.addEventListener('DOMContentLoaded', () => {
initApplication();
});