auto: 2026-09-28 17:00 (ESD_LAPTOP)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 실측:
|
||||
|
||||
@@ -194,6 +194,7 @@ let frameFields: Record<string, string> = {};
|
||||
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` 이라 안 두면 커서가 안 보임.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
{ 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);
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,8 @@ export interface CadLoadExtra {
|
||||
panelsCollapsed?: boolean;
|
||||
/** 미리보기 — 왼쪽 끌기 = 화면 옮기기 · 두 번 누름 = 전체 맞춤 · 선택 · 명령 안내 없음. */
|
||||
viewOnly?: boolean;
|
||||
/** 좌표 0,0 에 기준점 십자를 그린다(그리기 겹판 · 저장 안 됨) — 안 보내면 없음. */
|
||||
originMarker?: boolean;
|
||||
}
|
||||
|
||||
export interface CadHost<D extends CadHostDrawing, M, Q> {
|
||||
|
||||
Reference in New Issue
Block a user