- DXF -> 도각 JSON 변환 엔진 신설 (ezdxf) — 선·폴리선·원/호/타원/스플라인 평탄화·글자·점 변환, 블록·치수는 분해, 해치 등 미지원 요소는 제외
- DWG 는 ODA File Converter(.env ODA_CONVERTER_PATH) 경유 변환, 미설치 시 DXF 저장 안내로 폴백
- POST /{project_id}/frame-template/import 신설 — 파일을 편집 화면용 도면으로 반환(저장은 기존 [완료] 경로 유지), 20MB·2만 도형 상한
- 도각 편집 띠에 「파일 불러오기」 추가, 자리표 팔레트 신설 (글자 14종·그림 4종을 도면 중앙에 배치 후 이동)
- CAD 텍스트 선택 시 회색 점선 외곽선 표시 (출력물에는 미포함)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
197 lines
6.2 KiB
TypeScript
197 lines
6.2 KiB
TypeScript
/**
|
|
* B07 도각 자리표 배치 — 프로그램 값이 들어갈 자리를 사용자가 직접 놓는다 (2026-09-06 사용자 확정).
|
|
*
|
|
* 값을 알아맞히는 규칙은 만들지 않는다. 여기서 놓은 `{{키}}` 토큰을 출력 때 기존 치환
|
|
* 엔진(`_fill_placeholders`)이 그대로 채운다. 자리표를 안 놓은 값은 빈칸으로 남는다.
|
|
*
|
|
* 놓는 방식 — 단추를 누르면 도면 한가운데에 자리표가 서고, 그 뒤 캐드의 이동·크기 도구로
|
|
* 자리를 잡는다. 캐드 안쪽 코드는 건드리지 않는다(도면을 통째로 다시 싣는 방식).
|
|
*/
|
|
|
|
import { createButton, showToast } from "@ui/ui_template_elements";
|
|
import type { CadDrawing } from "./B07_DesignDetail_Api_Fetch";
|
|
|
|
/** 글자 자리표 — 출력 때 표제란 값으로 바뀐다. */
|
|
const TEXT_TOKENS: readonly string[] = [
|
|
"도면명",
|
|
"도면번호",
|
|
"공사명",
|
|
"위치",
|
|
"시행청",
|
|
"용역회사",
|
|
"연도기번",
|
|
"사업량",
|
|
"과업책임자",
|
|
"분야별책임자",
|
|
"설계자",
|
|
"설계일자",
|
|
"축척_A1",
|
|
"축척_A3",
|
|
];
|
|
|
|
/** 그림 자리표 — 회사 로고와 사람 서명. 값이 없으면 그림째 빠진다. */
|
|
const IMAGE_TOKENS: readonly string[] = [
|
|
"회사로고",
|
|
"과업책임자서명",
|
|
"분야별책임자서명",
|
|
"설계자서명",
|
|
];
|
|
|
|
const TEXT_SIZE_MM = 5;
|
|
const IMAGE_WIDTH_MM = 32;
|
|
const IMAGE_HEIGHT_MM = 16;
|
|
/** 겹쳐 놓지 않도록 하나 놓을 때마다 이만큼 내려 찍는다. */
|
|
const STACK_STEP_MM = 8;
|
|
|
|
interface Options {
|
|
/** CAD에서 현재 편집본을 받아온다. */
|
|
requestCadDrawing: () => Promise<CadDrawing>;
|
|
/** CAD에 도면을 다시 싣는다. */
|
|
sendLoad: (drawing: CadDrawing, meta: null) => void;
|
|
}
|
|
|
|
interface Point {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
/** 엔티티 목록의 한가운데 — 자리표를 처음 놓는 자리. 좌표가 없으면 원점. */
|
|
function centerOf(entities: Record<string, unknown>[]): Point {
|
|
const xs: number[] = [];
|
|
const ys: number[] = [];
|
|
const visit = (entity: Record<string, unknown>): void => {
|
|
const shape = (entity.shapeData ?? {}) as Record<string, unknown>;
|
|
for (const key of ["startPoint", "endPoint", "basePoint", "point"]) {
|
|
const value = shape[key] as Point | undefined;
|
|
if (value && typeof value.x === "number" && typeof value.y === "number") {
|
|
xs.push(value.x);
|
|
ys.push(value.y);
|
|
}
|
|
}
|
|
for (const vertex of (shape.points as Point[] | undefined) ?? []) {
|
|
if (vertex && typeof vertex.x === "number") {
|
|
xs.push(vertex.x);
|
|
ys.push(vertex.y);
|
|
}
|
|
}
|
|
for (const child of (entity.children as Record<string, unknown>[] | undefined) ?? []) {
|
|
visit(child);
|
|
}
|
|
};
|
|
for (const entity of entities) visit(entity);
|
|
if (xs.length === 0) return { x: 0, y: 0 };
|
|
const mid = (values: number[]): number => (Math.min(...values) + Math.max(...values)) / 2;
|
|
return { x: mid(xs), y: mid(ys) };
|
|
}
|
|
|
|
function layerIdOf(drawing: CadDrawing): string {
|
|
return drawing.layers[0]?.id ?? "0";
|
|
}
|
|
|
|
function textEntity(token: string, at: Point, layerId: string): Record<string, unknown> {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
type: "Text",
|
|
lineColor: "#f5f7fa",
|
|
lineWidth: 1,
|
|
layerId,
|
|
shapeData: {
|
|
label: `{{${token}}}`,
|
|
basePoint: { x: at.x, y: at.y },
|
|
options: {
|
|
textDirection: { x: 1, y: 0 },
|
|
textAlign: "center",
|
|
textColor: "#f5f7fa",
|
|
fontSize: TEXT_SIZE_MM,
|
|
fontFamily: "sans-serif",
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function imageEntity(token: string, at: Point, layerId: string): Record<string, unknown> {
|
|
const halfWidth = IMAGE_WIDTH_MM / 2;
|
|
const halfHeight = IMAGE_HEIGHT_MM / 2;
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
type: "Image",
|
|
lineColor: "#f5f7fa",
|
|
lineWidth: 1,
|
|
layerId,
|
|
shapeData: {
|
|
points: [
|
|
{ x: at.x - halfWidth, y: at.y - halfHeight },
|
|
{ x: at.x + halfWidth, y: at.y - halfHeight },
|
|
{ x: at.x + halfWidth, y: at.y + halfHeight },
|
|
{ x: at.x - halfWidth, y: at.y + halfHeight },
|
|
],
|
|
imageData: `{{${token}}}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface PlaceholderPalette {
|
|
/** 도각 편집 띠 아래에 붙는 자리표 목록 (평소엔 숨김). */
|
|
root: HTMLElement;
|
|
setVisible: (visible: boolean) => void;
|
|
}
|
|
|
|
export function createPlaceholderPalette(options: Options): PlaceholderPalette {
|
|
const root = document.createElement("div");
|
|
root.className = "b07-frame-tokens";
|
|
root.hidden = true;
|
|
|
|
const hint = document.createElement("span");
|
|
hint.className = "b07-frame-tokens__hint";
|
|
hint.textContent = "자리표 놓기 — 누르면 도면 가운데에 서고, 끌어서 자리를 잡음";
|
|
root.append(hint);
|
|
|
|
let placed = 0;
|
|
|
|
const place = async (token: string, kind: "text" | "image"): Promise<void> => {
|
|
try {
|
|
const drawing = await options.requestCadDrawing();
|
|
const center = centerOf(drawing.entities);
|
|
const at = { x: center.x, y: center.y - placed * STACK_STEP_MM };
|
|
const layerId = layerIdOf(drawing);
|
|
const entity =
|
|
kind === "text" ? textEntity(token, at, layerId) : imageEntity(token, at, layerId);
|
|
options.sendLoad({ ...drawing, entities: [...drawing.entities, entity] }, null);
|
|
placed += 1;
|
|
showToast(`「${token}」 자리표를 놓았습니다. 끌어서 자리를 잡으십시오.`, "success");
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : "자리표를 놓지 못했습니다.", "error");
|
|
}
|
|
};
|
|
|
|
const group = (label: string, tokens: readonly string[], kind: "text" | "image"): void => {
|
|
const box = document.createElement("div");
|
|
box.className = "b07-frame-tokens__group";
|
|
const title = document.createElement("span");
|
|
title.className = "b07-frame-tokens__title";
|
|
title.textContent = label;
|
|
box.append(title);
|
|
for (const token of tokens) {
|
|
box.append(
|
|
createButton({
|
|
label: token,
|
|
variant: "ghost",
|
|
onClick: () => void place(token, kind),
|
|
}),
|
|
);
|
|
}
|
|
root.append(box);
|
|
};
|
|
|
|
group("글자", TEXT_TOKENS, "text");
|
|
group("그림", IMAGE_TOKENS, "image");
|
|
|
|
return {
|
|
root,
|
|
setVisible: (visible: boolean) => {
|
|
root.hidden = !visible;
|
|
if (!visible) placed = 0;
|
|
},
|
|
};
|
|
}
|