사용자 편의성 점검 15건 중 반영 확정분 12건. 편집 손실 방지 - CAD에 미저장 표시(drawingDirty)를 두고, 도면을 바꾸기 전에 묻는다. 전에는 그은 선이 경고 없이 사라졌다(실측: 캔버스 서명이 원본과 동일). - 도면 전환을 겹쳐 눌러도 늦게 온 응답이 화면을 덮지 않게 가드를 뒀다. - 도면층 삭제·객체 이동을 되돌리기에 실어 Ctrl+Z로 돌아오게 했다. 확정한 도면은 읽기 전용 - runCommand 한 곳에서 보는 명령만 통과시킨다(허용 명시 방식). - 도면을 실을 때 앞 도면의 그리기 도구를 내린다 — 켜 둔 도구가 확정본에도 계속 그렸다. - 확정 버튼 자리를 [현재 도면 확정] / [수정]으로 가른다. 확정 해제는 [수정] 한 곳뿐 — 되돌리기·색 고르기로 확정이 풀리던 경로를 없앴다. 그 밖 - 잠금 도면층(도각·등고선·계류)은 마우스가 스쳐도 하이라이트하지 않는다. - 준비 중 도면 7종도 빈 도각으로 열린다(눌리지 않는 회색 버튼 제거). - 도면층 행이 잘리지 않게 패널을 300px로 넓히고 이름을 줄여 담는다. - 기본 그리기 색을 검정으로 — 흰 종이에 흰 선이라 안 보였다. - 색 고르는 동안 변경 통지가 연발하지 않는다. - 수량표는 앞 단계 산출물이라 도면에서 고치지 못하게 막고 안내한다. - 자동백업 칸을 도면별로 나눈다 — 되살리면 다른 도면에 붙었다. - 횡단 장 id를 시작 측점 기준으로 바꿔 구간이 달라져도 옛 확정이 안 붙는다. - 회사 도각 저장 전에 좌표를 검사하고, 기본 도각으로 되돌리기를 연다. - 횡단 파일이 없어도 나머지 도면 목록은 남는다. - 불러오는 중·실패를 화면에 표시한다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
174 lines
7.7 KiB
TypeScript
174 lines
7.7 KiB
TypeScript
import { Point } from '@flatten-js/core';
|
|
import { type DesignMeta, HtmlEvent } from '../App.types.ts';
|
|
import { TextEntity } from '../entities/TextEntity.ts';
|
|
import type { JsonDrawingFileSerialized } from '../helpers/import-export-handlers/export-entities-to-json.ts';
|
|
import { exportEntitiesAndLayersToJsonString } from '../helpers/import-export-handlers/export-entities-to-json.ts';
|
|
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json.ts';
|
|
import {
|
|
clearDrawingDirty,
|
|
getCanvas,
|
|
getDesignMeta,
|
|
getEntities,
|
|
getLayers,
|
|
getScreenCanvasDrawController,
|
|
isDrawingDirty,
|
|
isDrawingReadOnly,
|
|
resetUndoBaseline,
|
|
setActiveLayerId,
|
|
setDesignMeta,
|
|
setEntities,
|
|
setLayers,
|
|
} from '../state.ts';
|
|
import { toast } from 'react-toastify';
|
|
import { runCommandInput } from '../commands/run-command.ts';
|
|
import { setRecoveryScope } from '../helpers/autosave.ts';
|
|
|
|
export const AISLO_DRAWING_LOAD_MESSAGE = 'aislo:b08:load-drawing';
|
|
export const AISLO_DRAWING_READY_MESSAGE = 'aislo:b08:drawing-ready';
|
|
export const AISLO_DRAWING_LOADED_MESSAGE = 'aislo:b08:drawing-loaded';
|
|
export const AISLO_DRAWING_ERROR_MESSAGE = 'aislo:b08:drawing-error';
|
|
export const AISLO_DRAWING_CHANGED_MESSAGE = 'aislo:b08:drawing-changed';
|
|
export const AISLO_DRAWING_SAVE_REQUEST_MESSAGE = 'aislo:b08:save-request';
|
|
export const AISLO_DRAWING_SAVE_RESPONSE_MESSAGE = 'aislo:b08:save-response';
|
|
export const AISLO_DRAWING_NAVIGATE_MESSAGE = 'aislo:b08:navigate';
|
|
|
|
interface DrawingLoadMessage {
|
|
type: typeof AISLO_DRAWING_LOAD_MESSAGE;
|
|
drawing: JsonDrawingFileSerialized;
|
|
meta?: DesignMeta | null;
|
|
}
|
|
|
|
interface DrawingSaveRequestMessage {
|
|
type: typeof AISLO_DRAWING_SAVE_REQUEST_MESSAGE;
|
|
}
|
|
|
|
function isDrawingLoadMessage(value: unknown): value is DrawingLoadMessage {
|
|
if (!value || typeof value !== 'object') return false;
|
|
const candidate = value as Partial<DrawingLoadMessage>;
|
|
return candidate.type === AISLO_DRAWING_LOAD_MESSAGE && Boolean(candidate.drawing);
|
|
}
|
|
|
|
function notifyParent(type: string, payload: Record<string, unknown> = {}) {
|
|
if (window.parent === window) return;
|
|
window.parent.postMessage({ type, ...payload }, window.location.origin);
|
|
}
|
|
|
|
/** 수량 패널에서 이전/다음 도면으로 이동 요청 (부모가 처리). */
|
|
export function requestDrawingNavigation(direction: 'prev' | 'next') {
|
|
notifyParent(AISLO_DRAWING_NAVIGATE_MESSAGE, { direction });
|
|
}
|
|
|
|
/**
|
|
* 수량 산출표 도면층 — 여기 글자는 앞 단계(B05·B06) 산출값이라 B07에서 고치지 않는다
|
|
* (2026-09-01 사용자 확정). 고치면 그림 글자만 바뀌고 저장되는 수량표는 그대로여서
|
|
* 화면과 저장값이 어긋났다.
|
|
*/
|
|
const QUANTITY_TABLE_LAYER_ID = 'b08-cross-table';
|
|
|
|
/**
|
|
* 더블클릭한 위치의 텍스트(잠금 해제 레이어)를 찾아 내용을 즉시 편집한다.
|
|
* CAD 측점 테이블 값 셀 수정 수단 — 잠금 레이어와 수량 산출표는 제외.
|
|
*/
|
|
function registerTextDoubleClickEdit() {
|
|
const canvas = getCanvas();
|
|
if (!canvas) return;
|
|
canvas.addEventListener('dblclick', (event: MouseEvent) => {
|
|
if (isDrawingReadOnly()) {
|
|
toast.info('확정한 도면입니다. 고치려면 [수정]을 먼저 누르세요.');
|
|
return;
|
|
}
|
|
const drawController = getScreenCanvasDrawController();
|
|
const bounds = canvas.getBoundingClientRect();
|
|
const screenPoint = new Point(event.clientX - bounds.left, bounds.bottom - event.clientY);
|
|
const worldPoint = drawController.targetToWorld(screenPoint);
|
|
const lockedLayerIds = new Set(
|
|
getLayers()
|
|
.filter((layer) => layer.isLocked)
|
|
.map((layer) => layer.id)
|
|
);
|
|
let closest: TextEntity | null = null;
|
|
let closestDistance = Number.MAX_SAFE_INTEGER;
|
|
let closestOnQuantityTable = false;
|
|
for (const entity of getEntities()) {
|
|
if (!(entity instanceof TextEntity) || lockedLayerIds.has(entity.layerId)) continue;
|
|
const distanceInfo = entity.distanceTo(worldPoint);
|
|
if (distanceInfo && distanceInfo[0] < closestDistance) {
|
|
closestDistance = distanceInfo[0];
|
|
closest = entity;
|
|
closestOnQuantityTable = entity.layerId === QUANTITY_TABLE_LAYER_ID;
|
|
}
|
|
}
|
|
if (!closest) return;
|
|
const fontSize = closest.getTextOptions().fontSize;
|
|
const tolerance = Math.max((closest.getLabel().length + 2) * fontSize * 0.5, fontSize * 2);
|
|
if (closestDistance > tolerance) return;
|
|
if (closestOnQuantityTable) {
|
|
toast.info('수량표는 횡단설계(B06)에서 고칩니다. 고치고 돌아오면 반영됩니다.');
|
|
return;
|
|
}
|
|
const nextLabel = window.prompt('값 수정', closest.getLabel());
|
|
if (nextLabel === null || nextLabel === closest.getLabel()) return;
|
|
closest.setLabel(nextLabel);
|
|
setEntities([...getEntities()], true); // undo 스택 + DRAWING_CHANGED 통지
|
|
});
|
|
}
|
|
|
|
/**
|
|
* B08 parent page와 CAD 앱 사이의 same-origin JSON 경계다.
|
|
* DXF/DWG 파일이나 파서 객체는 이 경계를 통과하지 않는다.
|
|
*/
|
|
export function registerAisloDrawingBridge() {
|
|
window.addEventListener('message', async (event: MessageEvent<unknown>) => {
|
|
if (event.origin !== window.location.origin || event.source !== window.parent) return;
|
|
const message = event.data as Partial<DrawingSaveRequestMessage>;
|
|
if (message.type === AISLO_DRAWING_SAVE_REQUEST_MESSAGE) {
|
|
try {
|
|
const drawing = JSON.parse(
|
|
await exportEntitiesAndLayersToJsonString()
|
|
) as JsonDrawingFileSerialized;
|
|
// 수량표는 앞 단계 산출값을 그대로 돌려준다 — B07에서는 고치지 않는다.
|
|
const quantityTable = getDesignMeta()?.quantityTable ?? null;
|
|
clearDrawingDirty(); // 부모가 받아 저장한다 — 미저장 경고 대상이 아니다
|
|
notifyParent(AISLO_DRAWING_SAVE_RESPONSE_MESSAGE, { drawing, quantityTable });
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : 'Unable to serialize drawing';
|
|
notifyParent(AISLO_DRAWING_ERROR_MESSAGE, { detail });
|
|
}
|
|
return;
|
|
}
|
|
if (!isDrawingLoadMessage(event.data)) return;
|
|
|
|
try {
|
|
const drawing = await getEntitiesAndLayersFromJsonObject(event.data.drawing);
|
|
setEntities(drawing.entities, false);
|
|
setLayers(drawing.layers);
|
|
setActiveLayerId(drawing.layers[0].id);
|
|
// 실은 도면이 되돌리기의 바닥 — 첫 Ctrl+Z가 백지로 가지 않게 한다.
|
|
// (미저장 표시도 여기서 내려간다 — 방금 서버 도면을 실었으니 고친 것이 없다.)
|
|
resetUndoBaseline();
|
|
// 설계 컨텍스트(제목·측점정보·확정상태·수량표)를 수량 패널에 반영
|
|
setDesignMeta(event.data.meta ?? null);
|
|
// 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도**
|
|
// 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측:
|
|
// 확정본에서 클릭 두 번에 선 2개가 늘었다). 새 도면에서 앞 도면의 작도 도중
|
|
// 상태를 이어 갈 이유도 없다.
|
|
runCommandInput('SELECT');
|
|
// 자동백업을 이 도면 칸으로 옮긴다 — 안 옮기면 백업 한 칸을 서로 덮는다.
|
|
setRecoveryScope(event.data.meta?.drawingId ?? null);
|
|
getScreenCanvasDrawController().zoomToFitScreen();
|
|
notifyParent(AISLO_DRAWING_LOADED_MESSAGE);
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : 'Invalid drawing JSON';
|
|
notifyParent(AISLO_DRAWING_ERROR_MESSAGE, { detail });
|
|
}
|
|
});
|
|
// 부모는 이 통지로 **미저장 여부만** 안다. 확정을 푸는 것은 [수정] 버튼 한 곳뿐이다
|
|
// (2026-09-01 사용자 확정) — 예전에는 되돌리기·색 고르기까지 확정을 풀었다.
|
|
window.addEventListener(HtmlEvent.DRAWING_CHANGED, () => {
|
|
notifyParent(AISLO_DRAWING_CHANGED_MESSAGE, { dirty: isDrawingDirty() });
|
|
});
|
|
registerTextDoubleClickEdit();
|
|
|
|
notifyParent(AISLO_DRAWING_READY_MESSAGE);
|
|
}
|