Files
Aislo/common_util/common_util_structure_attach.ts
T
eomsangdonandClaude Opus 5.5 403722e687 refactor(B05·B06): 소단 = 옹벽처럼 측점에 제원 얹음 — 형상 얹기 한 벌 · 세션 사본 berm 걷음 (PLAN 577-2 · 요구 434 · 437)
- 얹기: attachShapes · attach_shapes 에 소단(section.berm · 덮는 소단 없음 = null) · SHAPE_KEYS 에 berm · TS attachBermSpecs ↔ 파이썬 attach_berm_specs · berm_spans_from 짝
- 읽기: 저장 노선 다시 셈(브라우저 · Node cross.refresh) = 얹힌 section.berm(칸 없음 = 저장분 design.berm) · 노선 초안 · 서버 Node 묶음 cross = crossWithShapes 의 얹힌 제원(bermAt · crossWithBerm 걷음)
- 계단은 계산 안쪽 그대로 · [저장] 길(구조물 정본 → cross.berm_spans) 그대로
- 지움: 시설 줄 syncBerm · putBerm(C4.copy) · writeBermSpans · 세션 칸 berm · bermRequest · design_shared bermAt
- 시험: test_272_berm_source 측점 얹기 거울로 · test_323_4 소단 얹음 · 477 · 511-A · 472 떠남 · 365 · 272 고름 헬퍼가 사본 대신 얹힌 소단 · design_pipeline_page 700 줄 → 699(주석 한 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:33:06 +09:00

174 lines
7.0 KiB
TypeScript

/* =============================================================================
* common_util/common_util_structure_attach.ts
* 형상 얹기 한 벌(323-4) — 측점(횡단) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료.
*
* 구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 서버와 같은 차례로 다시 얹음 — 관 세트 → 벽 →
* 포장 → 맹암거 → 소단 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀
* (`B06_Section_Section_Drafts.withDraftShapes`) — 저장 전 그림 = 저장 뒤 그림.
* 파이썬 짝 = `B06_Section/B06_Section_Engine_Attach.py` `attach_shapes`(서버 상세 읽기 · 재생성 응답) ·
* 거울 시험 = `resources/tester/test_323_4_attach_mirror.py`.
* ========================================================================== */
import {
attachCulvertSets,
buildCulvertSets,
registryDefaults,
type CulvertPipePoint,
} from "./common_util_culvert_sets";
import {
attachWallSpecs,
wallSpecsFrom,
type WallStructureInput,
} from "./common_util_structure_walls";
import {
attachPavementSpecs,
pavementSpecsFrom,
type PavementStructureInput,
} from "./common_util_structure_pavement";
import {
attachUnderdrainSpecs,
underdrainSpecsFrom,
type UnderdrainInput,
} from "./common_util_structure_underdrain";
import { attachBermSpecs, bermSpansFromStructures } from "./common_util_berm_spans";
import { catalogRow, fieldName } from "./common_util_structure_catalog";
import {
blankKeys,
planStations,
SAME_STATION_M,
type PlanPipe,
type PlanStructure,
type PlanType,
} from "./common_util_structure_station";
import { SHAPE_KEYS } from "./common_util_structure_shape_keys";
export { SHAPE_KEYS };
export interface AttachType extends PlanType {
name: string;
options: ReadonlyArray<{ key: string; label?: string; default: unknown }>;
}
/** 빈 필수 칸 → 「종류 이름 칸 이름」(칸 이름 = 그 종류 정의 → 다른 종류 정의 → 부품 상자 → 키). */
function blankLabels(
typeId: string,
options: Record<string, unknown> | null | undefined,
types: ReadonlyArray<AttachType>,
): string[] {
const name = catalogRow(typeId)?.name ?? typeId;
const ordered = [...types.filter((t) => t.type_id === typeId), ...types];
return blankKeys(typeId, options).map((key) => {
const option = ordered.flatMap((t) => t.options).find((o) => o.key === key);
return `${name} ${option?.label ?? fieldName(key) ?? key}`;
});
}
/** 빈칸 경고 — 측점 계획 자리마다 덮는 관 · 구조물의 빈 필수 칸(파이썬 `_attach_blanks` 짝). */
function attachBlanks(
sections: Array<Record<string, unknown>>,
pipes: ReadonlyArray<PlanPipe>,
structures: ReadonlyArray<PlanStructure>,
types: ReadonlyArray<AttachType>,
): void {
const owners = new Map<string, [string, Record<string, unknown> | null | undefined]>();
for (const pipe of pipes)
owners.set(pipe.id ?? `pipe-${pipe.chainage_m.toFixed(2)}`, [
pipe.facility ?? "pipe",
pipe.options,
]);
for (const item of structures)
if (item.structure_id) owners.set(item.structure_id, [item.type_id, item.options]);
for (const station of planStations(pipes, structures, types)) {
if (!station.blank.length) continue;
const labels = [
...new Set(
station.owners.flatMap((owner) => {
const hit = owners.get(owner);
return hit ? blankLabels(hit[0], hit[1], types) : [];
}),
),
];
for (const section of sections)
if (Math.abs(Number(section.chainage_m) - station.chainage_m) < SAME_STATION_M)
section.blank = labels;
}
}
/** 안 얹은 관 · 시설 누가거리(458) — 시종점 밖으로 밀림 · 주인 측점이 0.51 m 넘게 멂. */
export interface AttachMisses {
outside: number[];
far: number[];
}
/** 주인 측점 거리 상한(m) — `design_selection.PIPE_MATCH_M` · 파이썬 `PIPE_MATCH_M` 짝. */
const PIPE_MATCH_M = 0.51;
/** 시종점 밖으로 밀린 관인가(458) — 시종점 측점 자리인데 좌표가 노선 진행 반대쪽으로 0.51 m 넘게 바깥
* (따라감이 누가거리를 0 · 끝에 붙여 둠 · 옆 거리는 안 봄 · 좌표 없으면 아님 · 파이썬 `_outside_route` 짝). */
function outsideRoute(
pipe: { chainage_m: number; x?: number | null; y?: number | null },
sections: ReadonlyArray<Record<string, unknown>>,
): boolean {
const placed = sections
.filter((s) => [s.chainage_m, s.center_x, s.center_y].every(Number.isFinite))
.sort((a, b) => Number(a.chainage_m) - Number(b.chainage_m));
if (placed.length < 2 || !Number.isFinite(pipe.x) || !Number.isFinite(pipe.y)) return false;
const ends: Array<[Record<string, unknown>, Record<string, unknown>]> = [
[placed[0], placed[1]],
[placed[placed.length - 1], placed[placed.length - 2]],
];
return ends.some(([end, next]) => {
if (Math.abs(Number(end.chainage_m) - pipe.chainage_m) >= PIPE_MATCH_M) return false;
const [ex, ey] = [Number(end.center_x), Number(end.center_y)];
const [dx, dy] = [Number(next.center_x) - ex, Number(next.center_y) - ey];
const span = Math.hypot(dx, dy);
return span > 0 && ((pipe.x! - ex) * dx + (pipe.y! - ey) * dy) / span <= -PIPE_MATCH_M;
});
}
/** 측점마다 그림 자료를 걷고 현황으로 다시 얹음(파이썬 `attach_shapes` 짝 · 제자리 고침) ·
* 시종점 밖 관 · 주인 측점 먼 관은 안 얹고 돌려줌(458 · 경고는 부르는 쪽). */
export function attachShapes(
sections: Array<Record<string, unknown>>,
allPipes: ReadonlyArray<PlanPipe>,
structures: ReadonlyArray<PlanStructure>,
types: ReadonlyArray<AttachType>,
): AttachMisses {
for (const section of sections) for (const key of [...SHAPE_KEYS, "blank"]) delete section[key];
const misses: AttachMisses = { outside: [], far: [] };
const pipes = allPipes.filter((pipe) => {
const out = outsideRoute(pipe as Parameters<typeof outsideRoute>[0], sections);
if (out) misses.outside.push(pipe.chainage_m);
return !out;
});
const registry = registryDefaults(types);
attachCulvertSets(
sections,
buildCulvertSets(pipes as readonly CulvertPipePoint[], registry),
misses.far,
);
const shaped = (shape: string) => types.filter((t) => catalogRow(t.type_id)?.shape === shape);
// 관 세트가 선 측점은 벽을 안 얹음(`attachWallSpecs`) — 차례 = 서버와 같음
attachWallSpecs(
sections,
wallSpecsFrom(
structures as unknown as WallStructureInput[],
new Map(shaped("wall").map((t) => [t.type_id, t.name] as const)),
),
);
attachPavementSpecs(
sections,
pavementSpecsFrom(
structures as unknown as PavementStructureInput[],
new Set(shaped("pavement").map((t) => t.type_id)),
),
);
attachUnderdrainSpecs(sections, underdrainSpecsFrom(structures as UnderdrainInput[]));
attachBermSpecs(
sections,
bermSpansFromStructures(structures as Parameters<typeof bermSpansFromStructures>[0]),
);
attachBlanks(sections, pipes, structures, types);
return misses;
}