- 까닭: 새로 만든 장은 서버 · CAD 걸름 모두 원지반이 잡힘 — 안 잡힌 것은 180-1 앞에 원지반 층 잠금째 저장된 확정 장 또는 반영 전 서버 · 페이지 메모리의 옛 도면
- 확정 장을 열 때 원지반 층 잠금 풂 · 측점 층 없는 확정 장은 stale_layers 안내([수정] 때 새 꼴)
- 장의 측점마다 층 하나(b08-station-{측점} · 「횡단 No.3」) = 원지반 · 계획선 · 구조물 · 암 그림(자식 선까지) · 종류는 색 · 선 꼴 · 표 · 글은 수량표 층 · 종류 층 지움
- 개체 id · groupId 그대로 · placement 에 layer_id · design_ids — 화면 덧그림도 측점 층 · 서버 설계선은 id 로 걷음
- DXF 층 이름 = 측점 층 id
- 새 시험 test_212 · 174 · 180 · 180_2 원지반 찾기를 색으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
116 lines
5.0 KiB
TypeScript
116 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* B07_DesignDetail_UI_Cad_Structures.ts
|
|
* 횡단도 **구조물 작도** — 배수관·기슭막이·세월교·BOX암거·물넘이포장.
|
|
*
|
|
* 산식은 새로 만들지 않는다. B06 화면이 쓰는 기하·그리기 함수를 **그대로** 불러
|
|
* 화면에 붙이지 않은 오프스크린 SVG에 그린 뒤, 그 도형을 CAD 엔티티로 옮긴다
|
|
* (2026-08-30 사용자 확정: 프론트에서 B06 산식 재사용 — 파이썬 포팅 금지).
|
|
* 앞으로 B06에 구조물·치수·글자가 붙으면 여기 손대지 않아도 도면에 따라온다.
|
|
*
|
|
* 자리 맞추기는 서버가 도면에 실어 보내는 `cross_placements`가 정한다:
|
|
* x_mm = offset*mm_per_m + ox, y_mm = (elev - dy)*mm_per_m + oy
|
|
* SVG는 y가 아래로 자라므로 그릴 때 부호를 뒤집고 수확할 때 되돌린다.
|
|
* ========================================================================== */
|
|
|
|
import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
|
import { loadSectionDetail } from "../B06_Section/B06_Section_Section_Store";
|
|
import {
|
|
DESIGN_LAYER_ID,
|
|
entityBox,
|
|
overlayEntities,
|
|
type Entity,
|
|
} from "./B07_DesignDetail_Cross_Overlay";
|
|
|
|
/** 서버가 도면에 실어 보내는 측점별 실좌표(m) → 종이(mm) 변환값. */
|
|
export interface CrossPlacement {
|
|
chainage_m: number;
|
|
ox: number;
|
|
oy: number;
|
|
dy: number;
|
|
mm_per_m: number;
|
|
x0: number;
|
|
x1: number;
|
|
/** 블록 테두리(종이 mm) [x0, y0, x1, y1] — 그림을 이 안으로 자른다. */
|
|
frame?: number[];
|
|
/** 측점 한 벌 묶음(180-1) — 덧그림도 같은 묶음으로 함께 골라짐. */
|
|
group_id?: string;
|
|
/** 측점 도면층(212-2) — 덧그림도 그 층에. */
|
|
layer_id?: string;
|
|
/** 갈아 끼울 서버 설계선 id(212-2) — 측점 층이면 층으로는 못 가림. */
|
|
design_ids?: string[];
|
|
}
|
|
|
|
/** 개체(자식 선까지)를 측점 층으로(212-2). */
|
|
function moveToLayer(entity: Entity, layerId: string): void {
|
|
entity.layerId = layerId;
|
|
for (const child of (entity.children as Entity[] | undefined) ?? []) moveToLayer(child, layerId);
|
|
}
|
|
|
|
/** 측점과 도면 배치를 같은 자리로 볼 허용 오차(m). 정본이 누가거리를 0.01m로 끊어 쓴다. */
|
|
const CHAINAGE_TOLERANCE_M = 0.02;
|
|
|
|
/**
|
|
* 도면에 구조물 엔티티를 얹는다 (제자리 수정). 배치 메타가 없거나 자료를 못 읽으면
|
|
* 아무것도 하지 않는다 — 구조물이 빠져도 도면 자체는 열려야 한다.
|
|
*/
|
|
export async function appendStructureEntities(
|
|
projectId: string,
|
|
routeId: number,
|
|
drawing: { entities: Record<string, unknown>[]; cross_placements?: CrossPlacement[] },
|
|
): Promise<number> {
|
|
const placements = drawing.cross_placements ?? [];
|
|
if (!placements.length) return 0;
|
|
let sections: CrossSection[];
|
|
try {
|
|
// B05·B06과 같은 공유 캐시를 쓴다 — 도면마다 새로 받으면 장을 넘길 때마다
|
|
// 종횡단 상세를 다시 내려받아 0.2초씩 더 걸린다(2026-08-30 실측).
|
|
sections = (await loadSectionDetail(projectId, routeId)).cross_sections;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
|
|
let added = 0;
|
|
const fresh: Entity[] = [];
|
|
const replaced: number[][] = [];
|
|
const designIds = new Set<string>();
|
|
for (const placement of placements) {
|
|
const section = sections.find(
|
|
(item) => Math.abs(item.chainage_m - placement.chainage_m) <= CHAINAGE_TOLERANCE_M,
|
|
);
|
|
const frame = placement.frame;
|
|
if (!section || !frame) continue;
|
|
const x = (offset: number): number => offset * placement.mm_per_m + placement.ox;
|
|
const y = (elevation: number): number =>
|
|
-((elevation - placement.dy) * placement.mm_per_m + placement.oy);
|
|
let entities: Entity[];
|
|
try {
|
|
const drawn = overlayEntities(section, sections, x, y, frame);
|
|
if (!drawn) continue;
|
|
entities = drawn;
|
|
} catch {
|
|
continue; // 한 측점의 기하 실패가 도면 전체를 막으면 안 된다.
|
|
}
|
|
if (!entities.length) continue;
|
|
if (placement.group_id) for (const entity of entities) entity.groupId = placement.group_id;
|
|
if (placement.layer_id) for (const entity of entities) moveToLayer(entity, placement.layer_id);
|
|
for (const id of placement.design_ids ?? []) designIds.add(id);
|
|
// 이 블록 설계선은 우리가 다시 그렸다 — 서버가 낸 트림 전 설계선은 걷어낸다.
|
|
replaced.push(frame);
|
|
fresh.push(...entities);
|
|
added += entities.length;
|
|
}
|
|
if (replaced.length) {
|
|
// 걷어내기는 새 엔티티를 넣기 **전에** 한다 — 뒤에 하면 우리 것까지 같이 지운다.
|
|
drawing.entities = drawing.entities.filter((entity) => {
|
|
if (entity.layerId !== DESIGN_LAYER_ID && !designIds.has(entity.id as string)) return true;
|
|
const box = entityBox(entity);
|
|
if (!box) return true;
|
|
return !replaced.some(
|
|
([fx0, fy0, fx1, fy1]) => box[0] >= fx0 && box[2] <= fx1 && box[1] >= fy0 && box[3] <= fy1,
|
|
);
|
|
});
|
|
}
|
|
drawing.entities.push(...fresh);
|
|
return added;
|
|
}
|