Files
Aislo/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts
T
eomsangdonandClaude Opus 5.5 dce63ccc21 feat(486 B07 CAD 화면): 제목줄 · 램프 · 하단 도면 칸 지움 · ↶ ↷ = 리본 탭 줄 오른쪽 · 손가락 단추 = 자판 없는 기기만 · 탭 줄 빈 자리 변수
- 제목줄(Aislo CAD · 부제 · 「현재 도면」 램프) 지움 · --cad-title-height 0 · 💾 · JSON 내보내기 = DOM 두고 숨김(QUICK_ACCESS_HIDDEN)
- 빠른 실행 = 리본 탭 줄 빈 자리 뒤 · 접기 단추 앞(줄 높이 26 그대로 · M02 hostSave 면 안 그림)
- 빈 자리([출력] 오른쪽 ~ ↶ 왼쪽) → .cad-host 에 --cad-tab-slot-left/right(iframe 끝 기준 px) — 부모 탭 줄 겹침 자리
- ☰ 비키기 패딩 = 리본 탭 줄로 · 손가락 단추 = (hover: none) and (pointer: coarse) 만
- 하단 도면 이름 · ‹ › · 확정 칸(QuantityPanel) · 이동 요청 지움
- 시험 test_cad_touch(미디어 조건) · test_cad_host_tabs(머리줄 · 빈 자리 변수) 고쳐 씀

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

216 lines
10 KiB
TypeScript

import { type DesignMeta, HtmlEvent } from '../App.types.ts';
import { ImageEntity } from '../entities/ImageEntity.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,
getFrameFields,
getScreenCanvasDrawController,
isDrawingDirty,
resetUndoBaseline,
setActiveLayerId,
setDesignMeta,
setEntities,
setFrameEditMode,
setHostReadOnly,
setHostSave,
setHostTitle,
setLayers,
setOriginMarker,
setViewOnly,
setBare,
} from '../state.ts';
import { setInspectorCollapsed, setRibbonCollapsed } from '../components/ui-state.ts';
import { setFitMinHeight } from '../drawControllers/fit-view.ts';
import { runCommandInput } from '../commands/run-command.ts';
import { setRecoveryScope } from '../helpers/autosave.ts';
import { registerBoxResizeDrag } from './box-resize-drag.ts';
import { closeTextEditor, handleTextDoubleClick } from './text-inplace-edit.ts';
import { handleTableMessage } from './aislo-table-bridge.ts';
import { registerGuideDrag, setGuide, setGuideArea, setGuideVisible } from './guide-area.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_EXPORT_MESSAGE = 'aislo:b08:export-file';
export const AISLO_DRAWING_HOST_SAVE_MESSAGE = 'aislo:b08:host-save';
/** 작도 영역 가이드(169-1 · 169-3) — 부모 숫자 칸 → CAD 갈기 · CAD 끌기 → 부모 숫자 칸 */
export const AISLO_GUIDE_AREA_MESSAGE = 'aislo:b08:guide-area';
export const AISLO_GUIDE_CHANGED_MESSAGE = 'aislo:b08:guide-changed';
export const AISLO_GUIDE_VISIBLE_MESSAGE = 'aislo:b08:guide-visible';
interface DrawingLoadMessage {
type: typeof AISLO_DRAWING_LOAD_MESSAGE;
drawing: JsonDrawingFileSerialized;
meta?: DesignMeta | null;
/** 도각 편집으로 실은 도면인가 — 캐드 안 자리표 패널을 이때만 띄운다. */
frameEdit?: boolean;
/** 자리표에 보여 줄 실제 값 (편집 화면 전용 — 저장값은 토큰 그대로). */
frameFields?: Record<string, string>;
/** 자동백업 칸 이름 — 없으면 meta.drawingId. 수량 패널 없이 싣는 M02 양식이 쓴다. */
recoveryScope?: string;
/** 보기 전용으로 싣는가 — 확정본처럼 그리기·수정을 막는다. */
readOnly?: boolean;
/** 제목표시줄 부제 — 없으면 「B07 상세 설계」. */
hostTitle?: string;
/** 저장(💾 · Ctrl+S)을 부모 [저장]에 맡기는가. */
hostSave?: boolean;
/** 리본을 접힌 채로 여는가 — 없으면(B07) 펼침 그대로. */
ribbonCollapsed?: boolean;
/** 좌측 패널(특성 · 도면층)을 접힌 채로 여는가 — 없으면(B07) 지금 상태 그대로. */
panelsCollapsed?: boolean;
/** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
viewOnly?: boolean;
/** 캔버스만 — 제목줄 · 리본 · 패널 · 상태막대를 안 그림(B07 평면도 관리 모달). */
bare?: boolean;
originMarker?: boolean;
/** 맞춤 최소 높이(월드) — 글만 있는 작은 도면 과확대 막음(B06 상세 표 위 CAD · 380-4). */
fitMinHeight?: number;
/** 작도 영역 가이드 — {area, marks?, editable?} · 없으면 가이드 없음(`guide-area.ts`). */
guide?: unknown;
}
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 requestHostSave() {
notifyParent(AISLO_DRAWING_HOST_SAVE_MESSAGE);
}
/**
* 지금 도면을 DXF·DWG 파일로 내려받도록 부모에게 요청한다 (2026-09-06 사용자 지시).
* 캐드는 프로젝트를 모르므로 파일 만들기는 부모가 서버에 맡긴다.
*/
export function requestDrawingExport(fileFormat: 'dxf' | 'dwg') {
notifyParent(AISLO_DRAWING_EXPORT_MESSAGE, { fileFormat });
}
/** 글자 더블클릭 = 그 자리 입력 칸(`text-inplace-edit.ts` · 293-1). */
function registerTextDoubleClickEdit() {
const canvas = getCanvas();
if (!canvas) return;
canvas.addEventListener('dblclick', (event: MouseEvent) => handleTextDoubleClick(event, canvas));
}
/**
* 자리표에 실제 값을 입힌다 — 저장값은 그대로 두고 **보여 주기만** 바꾼다
* (2026-09-06 사용자 지시). 편집 화면에서 어디에 무엇이 들어가는지 보이게 하는 것이 목적.
*/
function applyFramePreview(): void {
const fields = getFrameFields();
if (Object.keys(fields).length === 0) return;
const token = /^\{\{(.+)\}\}$/;
for (const entity of getEntities()) {
if (entity instanceof TextEntity) {
const match = token.exec(entity.getLabel().trim());
const value = match ? fields[match[1]] : undefined;
entity.previewLabel = value ?? null;
} else if (entity instanceof ImageEntity && entity.isPlaceholder()) {
const match = token.exec((entity.getSourceData() ?? '').trim());
const value = match ? fields[match[1]] : undefined;
if (value) entity.setPreviewImage(value);
}
}
}
/**
* 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;
if (await handleTableMessage(event.data, notifyParent)) return; // 표 놓기 · 갈기(M02 41장)
const guideMessage = event.data as { type?: string; area?: unknown; visible?: unknown };
if (guideMessage?.type === AISLO_GUIDE_AREA_MESSAGE) return setGuideArea(guideMessage.area);
if (guideMessage?.type === AISLO_GUIDE_VISIBLE_MESSAGE)
return setGuideVisible(guideMessage.visible);
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;
closeTextEditor(false); // 옛 도면 글자 칸은 새 도면에 적용하지 않음
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);
setFrameEditMode(event.data.frameEdit === true, event.data.frameFields ?? {});
setHostReadOnly(event.data.readOnly === true);
setHostTitle(event.data.hostTitle ?? 'B07 상세 설계');
setHostSave(event.data.hostSave === true);
setRibbonCollapsed(event.data.ribbonCollapsed === true);
if (event.data.panelsCollapsed) setInspectorCollapsed(true);
setViewOnly(event.data.viewOnly === true);
setBare(event.data.bare === true);
setOriginMarker(event.data.originMarker === true);
setGuide(event.data.guide);
if (event.data.frameEdit) applyFramePreview();
// 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도**
// 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측:
// 확정본에서 클릭 두 번에 선 2개가 늘었다). 새 도면에서 앞 도면의 작도 도중
// 상태를 이어 갈 이유도 없다.
runCommandInput('SELECT');
// 자동백업을 이 도면 칸으로 옮긴다 — 안 옮기면 백업 한 칸을 서로 덮는다.
setRecoveryScope(event.data.recoveryScope ?? event.data.meta?.drawingId ?? null);
setFitMinHeight(event.data.fitMinHeight);
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();
registerBoxResizeDrag();
registerGuideDrag((area) => notifyParent(AISLO_GUIDE_CHANGED_MESSAGE, { area }));
notifyParent(AISLO_DRAWING_READY_MESSAGE);
}