From 4ea695907e4a58ac090932c702a5337d0cf29be8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 28 Sep 2026 17:00:57 +0900 Subject: [PATCH] auto: 2026-09-28 17:00 (ESD_LAPTOP) --- .../openwebcad/src/helpers/draw-functions.ts | 20 ++++++ .../openwebcad/src/helpers/draw.ts | 3 + .../src/integration/aislo-drawing-bridge.ts | 3 + B07_DesignDetail/openwebcad/src/state.ts | 7 ++ .../M02_MasterTemplete_Drawing.ts | 3 + .../M02_MasterTemplete_Structure.ts | 1 + .../M02_MasterTemplete_UI_Main.ts | 1 + resources/tester/cad/origin-marker.test.ts | 65 +++++++++++++++++++ ui_template/cad_host/cad_host.ts | 2 + 9 files changed, 105 insertions(+) create mode 100644 resources/tester/cad/origin-marker.test.ts diff --git a/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts b/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts index f3f217f2..47fae302 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/draw-functions.ts @@ -11,6 +11,9 @@ import { SNAP_POINT_SIZE, } from '../App.consts'; import { type SnapPoint, SnapPointType } from '../App.types'; + +/** 기준점 십자 반길이(px) — 가로세로 20px. */ +const ORIGIN_MARKER_SIZE = 10; import type { DrawController } from '../drawControllers/DrawController'; import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController'; import type { Entity } from '../entities/Entity'; @@ -229,6 +232,23 @@ export function drawCursor(drawController: ScreenCanvasDrawController) { ); } +/** 좌표 0,0 기준점 — 화면 크기 고정 십자 + 작은 원. 도면 개체가 아니라 그리기 겹판(저장 · 선택 · 스냅 대상 아님). */ +export function drawOriginMarker(drawController: ScreenCanvasDrawController) { + const origin = drawController.worldToTarget(new Point(0, 0)); + drawController.setOpacity(0.6); + drawController.setLineStyles(false, false, '#ffffff', 1, []); // 자동색 — 바탕 따라 흰/검정 + drawController.drawLineScreen( + new Point(origin.x - ORIGIN_MARKER_SIZE, origin.y), + new Point(origin.x + ORIGIN_MARKER_SIZE, origin.y) + ); + drawController.drawLineScreen( + new Point(origin.x, origin.y - ORIGIN_MARKER_SIZE), + new Point(origin.x, origin.y + ORIGIN_MARKER_SIZE) + ); + drawController.drawArcScreen(origin, ORIGIN_MARKER_SIZE / 3, 0, 2 * Math.PI, true); + drawController.setOpacity(1); +} + /** 선택한 객체의 그립(편집점)을 화면 크기 고정 사각형으로 그린다 */ export function drawGrips(drawController: ScreenCanvasDrawController, entities: Entity[]) { for (const entity of entities) { diff --git a/B07_DesignDetail/openwebcad/src/helpers/draw.ts b/B07_DesignDetail/openwebcad/src/helpers/draw.ts index 861ae433..51649bdb 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/draw.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/draw.ts @@ -13,6 +13,7 @@ import { getShouldDrawCursor, getSnapPoint, getSnapPointOnAngleGuide, + isOriginMarker, isViewOnly, } from '../state'; import { @@ -21,6 +22,7 @@ import { drawEntities, drawGrips, drawHelpers, + drawOriginMarker, drawSnapPoint, } from './draw-functions'; import { getClosestSnapPoint } from './get-closest-snap-point'; @@ -48,6 +50,7 @@ export function draw(drawController: ScreenCanvasDrawController) { drawScene(drawController, performance.now()); drawHighlightedEntities(drawController); + if (isOriginMarker()) drawOriginMarker(drawController); drawGrips(drawController, getSelectedEntities()); drawHelpers(drawController, getAngleGuideEntities()); drawEntities(drawController, getGhostHelperEntities()); diff --git a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts index 4fd4ad21..29f4bea0 100644 --- a/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts +++ b/B07_DesignDetail/openwebcad/src/integration/aislo-drawing-bridge.ts @@ -25,6 +25,7 @@ import { setHostSave, setHostTitle, setLayers, + setOriginMarker, setViewOnly, } from '../state.ts'; import { setInspectorCollapsed, setRibbonCollapsed } from '../components/ui-state.ts'; @@ -66,6 +67,7 @@ interface DrawingLoadMessage { panelsCollapsed?: boolean; /** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */ viewOnly?: boolean; + originMarker?: boolean; } interface DrawingSaveRequestMessage { @@ -223,6 +225,7 @@ export function registerAisloDrawingBridge() { setRibbonCollapsed(event.data.ribbonCollapsed === true); if (event.data.panelsCollapsed) setInspectorCollapsed(true); setViewOnly(event.data.viewOnly === true); + setOriginMarker(event.data.originMarker === true); if (event.data.frameEdit) applyFramePreview(); // 앞 도면에서 켜 둔 그리기 도구를 내린다. 안 내리면 **확정한 도면 위에도** // 그 도구가 계속 그린다 — 읽기 전용은 새 명령만 막기 때문이다(2026-09-01 실측: diff --git a/B07_DesignDetail/openwebcad/src/state.ts b/B07_DesignDetail/openwebcad/src/state.ts index a9d9a0b7..10b70c21 100644 --- a/B07_DesignDetail/openwebcad/src/state.ts +++ b/B07_DesignDetail/openwebcad/src/state.ts @@ -194,6 +194,7 @@ let frameFields: Record = {}; let hostReadOnly = false; /** 미리보기(M02 집계표 카드) — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */ let viewOnly = false; +let originMarker = false; let hostTitle = 'B07 상세 설계'; let hostSave = false; @@ -275,6 +276,8 @@ export const isDrawingDirty = () => drawingDirty; */ export const isDrawingReadOnly = (): boolean => designMeta?.confirmed === true || hostReadOnly; export const isViewOnly = (): boolean => viewOnly; +/** 좌표 0,0 기준점 십자를 그리는가 — 그리기 겹판일 뿐 도면 개체가 아니다. */ +export const isOriginMarker = (): boolean => originMarker; // setters export const setCanvas = (newCanvas: HTMLCanvasElement) => { @@ -518,6 +521,10 @@ export const setHostReadOnly = (readOnly: boolean) => { hostReadOnly = readOnly; notifyWindow(HtmlEvent.UPDATE_STATE); }; +export const setOriginMarker = (enabled: boolean) => { + originMarker = enabled; + notifyWindow(HtmlEvent.UPDATE_STATE); +}; export const setViewOnly = (enabled: boolean) => { viewOnly = enabled; // 그리는 십자 커서 대신 손 모양 — 캔버스는 `cursor-none` 이라 안 두면 커서가 안 보임. diff --git a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts index 63e1d9dd..fa8e902c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts @@ -43,6 +43,8 @@ export interface DrawingTemplateOptions { panelsCollapsed?: boolean; /** 미리보기(집계표 카드) — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */ preview?: boolean; + /** 좌표 0,0 기준점 십자를 그린다 — 구조물 도면(편집 · 미리보기)만. */ + originMarker?: boolean; } const RECOVERY_PREFIX = "OPEN_WEB_CAD__RECOVERY__"; @@ -201,6 +203,7 @@ export function mountDrawingTemplate( ribbonCollapsed: true, panelsCollapsed: options.panelsCollapsed, viewOnly: options.preview === true, + originMarker: options.originMarker === true, }, ); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index c584702a..82e22ff2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -378,6 +378,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane layer: "system", panelsCollapsed: true, // 특성 · 도면층 패널은 접힌 채 — › 를 누르면 펼침 preview: true, // 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 + originMarker: true, // 좌표 0,0 기준점 십자 }); const frame = previewHost.querySelector("iframe"); if (frame) hideToolbar(frame); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index fa5777cd..8f157215 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -235,6 +235,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle { projectId: at.projectId, onChanged: (d: boolean) => (dirty = d && !readOnly), headerSlot: slot, + originMarker: at.kind === "structure", }); return; } diff --git a/resources/tester/cad/origin-marker.test.ts b/resources/tester/cad/origin-marker.test.ts new file mode 100644 index 00000000..c909c36e --- /dev/null +++ b/resources/tester/cad/origin-marker.test.ts @@ -0,0 +1,65 @@ +/** + * 기준점 십자(좌표 0,0) 검증 — 확대해도 화면 크기 고정 · 원점에 붙음 · 도면 개체를 안 늘림. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --config vitest.tmp.config.ts origin-marker + */ +import { Point } from '@flatten-js/core'; +import { describe, expect, it, vi } from 'vitest'; +import { ScreenCanvasDrawController } from '../../../B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController'; +import { drawOriginMarker } from '../../../B07_DesignDetail/openwebcad/src/helpers/draw-functions'; +import { getEntities } from '../../../B07_DesignDetail/openwebcad/src/state'; + +// 자동색(theme.ts)이 `document` 의 data-theme 를 읽는다. +vi.stubGlobal('document', { documentElement: { getAttribute: () => 'dark' } }); + +/** moveTo·lineTo·arc 인자만 기록하는 최소 2d 컨텍스트 — 나머지 호출은 무시. */ +function recordingContext() { + const segments: number[][] = []; + const arcs: number[][] = []; + let from: number[] = []; + const context = new Proxy( + { + lineWidth: 1, + moveTo: (x: number, y: number) => (from = [x, y]), + lineTo: (x: number, y: number) => segments.push([...from, x, y]), + arc: (x: number, y: number, r: number) => arcs.push([x, y, r]), + } as Record, + { get: (target, key: string) => target[key] ?? (() => undefined), set: () => true } + ) as unknown as CanvasRenderingContext2D; + return { context, segments, arcs }; +} + +function drawAt(scale: number, offset: Point) { + const { context, segments, arcs } = recordingContext(); + const controller = new ScreenCanvasDrawController(context); + controller.setCanvasSize(new Point(800, 600)); + controller.setScreenScale(scale); + controller.setScreenOffset(offset); + drawOriginMarker(controller); + const origin = controller.worldToTarget(new Point(0, 0)); + return { segments, arcs, origin }; +} + +describe('기준점 십자', () => { + it.each([0.01, 1, 40])('확대 %s 배에서도 가로세로 20px', (scale) => { + const { segments, arcs } = drawAt(scale, new Point(-120, 75)); + expect(segments).toHaveLength(2); + const lengths = segments.map(([x1, y1, x2, y2]) => Math.hypot(x2 - x1, y2 - y1)); + expect(lengths).toEqual([20, 20]); + // 끝점 둥글림(반지름 0.5)을 뺀 원 하나 — 작은 원 + expect(arcs.filter(([, , r]) => r > 1).map(([, , r]) => r)).toEqual([10 / 3]); + }); + + it('십자 중심이 원점에 붙어 이동을 따라감', () => { + const a = drawAt(2, new Point(0, 0)); + const b = drawAt(2, new Point(50, -30)); + const center = ([x1, , x2]: number[]) => (x1 + x2) / 2; + expect(center(a.segments[0])).toBeCloseTo(a.origin.x); + expect(center(b.segments[0])).toBeCloseTo(b.origin.x); + expect(b.origin.x - a.origin.x).toBeCloseTo(-100); // 세계 50 = 화면 100px(2배) — 화면을 옮기면 원점이 반대로 감 + }); + + it('그리기만 — 도면 개체를 만들지 않음', () => { + drawAt(1, new Point(0, 0)); + expect(getEntities()).toHaveLength(0); + }); +}); diff --git a/ui_template/cad_host/cad_host.ts b/ui_template/cad_host/cad_host.ts index ba4d3675..b004bab5 100644 --- a/ui_template/cad_host/cad_host.ts +++ b/ui_template/cad_host/cad_host.ts @@ -69,6 +69,8 @@ export interface CadLoadExtra { panelsCollapsed?: boolean; /** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */ viewOnly?: boolean; + /** 좌표 0,0 에 기준점 십자를 그린다(그리기 겹판 · 저장 안 됨) — 안 보내면 없음. */ + originMarker?: boolean; } export interface CadHost {