fix(구조물표): 323-9 ④ 덧고침 — 노선 끝 관 3D 둘(관 카드 1078 · EP 카드 1078.010813) · 3D 몸체 = 같은 시설(얹을 때 적은 시설 누가거리)을 그리는 카드 중 시설 자리에 가장 가까운 하나만(ownsBody · 관 · 세월교 · BOX · 물넘이는 fordPavementChute 같은 규칙)

- B05 3D: ownsBodies(같은 m 만) → ownsBody(시설 누가거리로 묶음 · 차례 무관)
- 시험: test_323_9_twin_cards_3d 에 EP 꼴(0.010813 m 차 · 차례 바꿈) 더함 · 고치기 전 두 배 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 21:25:41 +09:00
co-authored by Claude Opus 5.5
parent dd35589079
commit 0d1c4f2615
4 changed files with 40 additions and 18 deletions
@@ -54,7 +54,7 @@ import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Reve
import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
import { chuteBands, fordPavementChute } from "./B05_Profile_UI_Corridor_Structures_Chute";
import { isChuteSegment } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
import { builds, culvertBody, ownsBodies } from "./B05_Profile_UI_Corridor_Structures_Pick";
import { builds, culvertBody, ownsBody } from "./B05_Profile_UI_Corridor_Structures_Pick";
/** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */
export interface StructureFrame {
@@ -279,12 +279,12 @@ export function buildCorridorStructures(
): CorridorStructure[] {
const solids: CorridorStructure[] = [];
for (const section of crossSections) {
// 몸체 고르기 = 그림 자료 열쇠의 주인 종류 표 3D 부품(322-1) · 같은 m 카드 둘이면 한 번(323-9)
const own = ownsBodies(section, crossSections);
const layout = own && culvertBody(section) ? culvertLayoutOf(section) : null;
// 몸체 고르기 = 그림 자료 열쇠의 주인 종류 표 3D 부품(322-1) · 같은 시설 카드 둘이면 한 번(323-9)
const own = (key: "culvert" | "ford" | "box"): boolean => ownsBody(section, crossSections, key);
const layout = own("culvert") && culvertBody(section) ? culvertLayoutOf(section) : null;
// 흙막이가 선 쪽은 횡단과 같은 자리 함수로 — 측벽 걷고 관 · 바닥판이 흙막이까지(89-1)
const fordLayout =
own && builds("ford_bridge", "ford")
own("ford") && builds("ford_bridge", "ford")
? withoutGuardedFill(fordLayoutOf(section), layout)
: null;
// 독립 기슭막이(옛 D군 경로) — 측점에 배관/숨김 기슭막이 세트가 붙으면 그쪽(위
@@ -299,9 +299,10 @@ export function buildCorridorStructures(
builds(section.revetment?.type_id, "wall")
? computeRevetmentLayout(section, section.design?.revet_adjust?.own)
: null;
const paveChute =
own && builds("ford_pavement", "chute") ? fordPavementChute(section, crossSections) : null;
const box = own && builds("box_culvert", "box") ? section.box : undefined;
const paveChute = builds("ford_pavement", "chute")
? fordPavementChute(section, crossSections)
: null;
const box = own("box") && builds("box_culvert", "box") ? section.box : undefined;
if (!layout && !fordLayout && !box && !revetLayout && !paveChute) continue;
const chainage = section.chainage_m;
const stationFrame: StructureFrame = {
@@ -12,9 +12,22 @@ import { catalogRow } from "../common_util/common_util_structure_catalog";
export const builds = (typeId: string | null | undefined, body: string): boolean =>
(catalogRow(typeId ?? "")?.model3d ?? body) === body;
/** 관 몸체를 세울 측점인가 — 같은 m 카드 둘(노선 끝 EP)이면 앞 카드만(관 하나에 한 번 · 323-9). */
export const ownsBodies = (section: CrossSection, sections: readonly CrossSection[]): boolean =>
sections.find((other) => other.chainage_m === section.chainage_m) === section;
/**
* 이 카드가 시설(관 · 세월교 · BOX) 몸체를 세우는가 — 같은 시설(얹을 때 적은 시설 누가거리)을 그리는 카드 중
* 시설 자리에 가장 가까운 하나만(노선 끝 EP 카드 0.01 m 차 · 323-9 · `fordPavementChute` 와 같은 규칙).
*/
export function ownsBody(
section: CrossSection,
sections: readonly CrossSection[],
key: "culvert" | "ford" | "box",
): boolean {
const at = section[key]?.chainage_m;
if (typeof at !== "number") return true; // 시설 자리 없음 = 옛 자료 그대로
const gap = (item: CrossSection): number => Math.abs(item.chainage_m - at);
const same = sections.filter((item) => item[key]?.chainage_m === at);
const owner = same.reduce((best, item) => (gap(item) < gap(best) ? item : best), same[0]);
return owner === section;
}
/** `culvert` 자리 주인 = 박스 · 세월교 흙막이(body) · 독립 기슭막이(관 숨김) · 배관. */
export function culvertBody(section: CrossSection): boolean {
@@ -1,4 +1,4 @@
/* PLAN 323-9 ④-② — 노선 끝(EP)처럼 같은 m 카드 둘이면 3D 관 · 세월교 · BOX 몸체는 한 번.
/* PLAN 323-9 ④-② — 같은 시설을 그리는 카드 둘(같은 m · 노선 끝 EP 꼴 소수 차 0.01 m)이면 3D 관 · 세월교 · BOX 몸체는 한 번.
* 화면 TS(`buildCorridorStructures`)를 그 자리에서 트랜스파일해 Node 로 돌림(로더 = helper_135_3 과 같음).
* 입력 = 73-4 픽스처(세월교 173.087 · BOX 352.14) · 결과 = 표준출력 JSON. 짝 = test_323_9_twin_cards_3d.py.
*/
@@ -74,14 +74,19 @@ const kinds = (sections) =>
.map((s) => `${s.kind}:${s.key ?? ""}:${s.chainage_m.toFixed(2)}`)
.sort();
const pick = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Corridor_Structures_Pick.ts"));
const own = pick.ownsBodies;
const own = pick.ownsBody;
/** 노선 끝 꼴 — 같은 시설을 그리는 카드가 소수 차(EP 1078 · 1078.010813)로 하나 더. */
const ep = (one) => Object.assign(clone(one), { chainage_m: one.chainage_m + 0.010813 });
const out = {};
for (const c of [173.087, 352.14]) {
const one = at(c);
const twin = kinds([one, clone(one)]);
pick.ownsBodies = () => true; // 고치기 전(측점마다 몸체) — 기준이 변경을 재는지
const epTwin = kinds([one, ep(one)]);
const epFirst = kinds([ep(one), one]); // 차례가 바뀌어도 시설 자리에 가까운 카드 하나
pick.ownsBody = () => true; // 고치기 전(카드마다 몸체) — 기준이 변경을 재는지
const before = kinds([one, clone(one)]);
pick.ownsBodies = own;
out[c] = { one: kinds([one]), twin, before };
const epBefore = kinds([one, ep(one)]);
pick.ownsBody = own;
out[c] = { one: kinds([one]), twin, before, epTwin, epFirst, epBefore };
}
process.stdout.write(JSON.stringify(out));
+5 -2
View File
@@ -1,6 +1,6 @@
"""PLAN 323-9 ④-② — 노선 끝(EP)처럼 같은 m 카드 둘이면 3D 관 · 세월교 · BOX 몸체가 둘 서던 흠.
"""PLAN 323-9 ④-② — 같은 시설을 그리는 카드 둘(같은 m · 노선 끝 EP 꼴 0.01 m 차)이면 3D 몸체가 둘 서던 흠.
3D 몸체는 구조물(관) 하나에 한 번 — 같은 m 카드 둘이면 앞 카드만 세움(`ownsBodies`).
3D 몸체는 시설(관 · 세월교 · BOX) 하나에 한 번 — 시설 자리에 가장 가까운 카드만 세움(`ownsBody`).
Node 헬퍼 = `helper_323_9_twin_cards_3d.cjs`(73-4 픽스처 · 진짜 `buildCorridorStructures`).
"""
@@ -38,5 +38,8 @@ def test_twin_cards_build_bodies_once(out: dict, at: str) -> None:
got = out[at]
assert got["one"] and got["twin"] == got["one"]
assert len(got["before"]) == 2 * len(got["one"]) # 고치기 전 = 두 번(시험이 변경을 잼)
# 노선 끝 EP 꼴 — 같은 시설 카드가 소수 차(0.010813 m) · 차례가 바뀌어도 시설 자리에 가까운 카드 하나
assert got["epTwin"] == got["one"] and got["epFirst"] == got["one"]
assert len(got["epBefore"]) == 2 * len(got["one"])
if at == "173.087": # 세월교 관 두 련 — 관 몸체도 한 번
assert sum(k.startswith("pipe:") for k in got["twin"]) == 2