Files
Aislo/B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts
T
eomsangdonandClaude Opus 5.5 41f0c3d6e3 feat(b07): 횡단 원지반 층 잠금 풂 · 측점 한 벌 groupId(180-1)
- 장 · 측점 한 장 원지반 층 잠금 지움(종단 그대로)
- 장의 측점마다 그림 · 표 · 글 groupId 하나 · cross_placements group_id
- 화면 덧그림(설계선 · 구조물)도 같은 groupId
- 새 시험 test_180_b07_cross_group

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
2026-10-10 11:46:49 +09:00

103 lines
4.3 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;
}
/** 측점과 도면 배치를 같은 자리로 볼 허용 오차(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[][] = [];
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;
// 이 블록 설계선은 우리가 다시 그렸다 — 서버가 낸 트림 전 설계선은 걷어낸다.
replaced.push(frame);
fresh.push(...entities);
added += entities.length;
}
if (replaced.length) {
// 걷어내기는 새 엔티티를 넣기 **전에** 한다 — 뒤에 하면 우리 것까지 같이 지운다.
drawing.entities = drawing.entities.filter((entity) => {
if (entity.layerId !== DESIGN_LAYER_ID) 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;
}