fix(구조물표): 323-9 ④ 작은 것 — 검사 글 띄어쓰기 「… · 구조물에 안 맞음」(화면 · 서버 짝) · 노선 끝(EP)처럼 같은 m 카드 둘이면 3D 관 · 세월교 · BOX 몸체를 한 번만(앞 카드 · ownsBodies)
- M02_SO_Def_Check.ts ↔ M02_SO_Check.py 글 · test_322_2_check_mirror 기대 글 - B05 3D: buildCorridorStructures 관 몸체 = 같은 m 앞 카드만 · 벽은 그대로(wallSweepAt) - 시험: test_323_9_twin_cards_3d(세월교 · BOX 카드 둘 = 하나와 같음 · 고치기 전 두 배) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -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 } from "./B05_Profile_UI_Corridor_Structures_Pick";
|
||||
import { builds, culvertBody, ownsBodies } from "./B05_Profile_UI_Corridor_Structures_Pick";
|
||||
|
||||
/** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */
|
||||
export interface StructureFrame {
|
||||
@@ -279,12 +279,14 @@ export function buildCorridorStructures(
|
||||
): CorridorStructure[] {
|
||||
const solids: CorridorStructure[] = [];
|
||||
for (const section of crossSections) {
|
||||
// 몸체 고르기 = 그림 자료 열쇠의 주인 종류 표 3D 부품(322-1)
|
||||
const layout = culvertBody(section) ? culvertLayoutOf(section) : null;
|
||||
// 몸체 고르기 = 그림 자료 열쇠의 주인 종류 표 3D 부품(322-1) · 같은 m 카드 둘이면 한 번(323-9)
|
||||
const own = ownsBodies(section, crossSections);
|
||||
const layout = own && culvertBody(section) ? culvertLayoutOf(section) : null;
|
||||
// 흙막이가 선 쪽은 횡단과 같은 자리 함수로 — 측벽 걷고 관 · 바닥판이 흙막이까지(89-1)
|
||||
const fordLayout = builds("ford_bridge", "ford")
|
||||
? withoutGuardedFill(fordLayoutOf(section), layout)
|
||||
: null;
|
||||
const fordLayout =
|
||||
own && builds("ford_bridge", "ford")
|
||||
? withoutGuardedFill(fordLayoutOf(section), layout)
|
||||
: null;
|
||||
// 독립 기슭막이(옛 D군 경로) — 측점에 배관/숨김 기슭막이 세트가 붙으면 그쪽(위
|
||||
// culvertLayoutOf)이 그린다. 아직 이관 안 된 정본만 이 옛 경로로 스윕한다
|
||||
// (2026-08-28 이관 이중그리기 방지 — 횡단도 가드와 같은 규칙).
|
||||
@@ -297,10 +299,9 @@ export function buildCorridorStructures(
|
||||
builds(section.revetment?.type_id, "wall")
|
||||
? computeRevetmentLayout(section, section.design?.revet_adjust?.own)
|
||||
: null;
|
||||
const paveChute = builds("ford_pavement", "chute")
|
||||
? fordPavementChute(section, crossSections)
|
||||
: null;
|
||||
const box = builds("box_culvert", "box") ? section.box : undefined;
|
||||
const paveChute =
|
||||
own && builds("ford_pavement", "chute") ? fordPavementChute(section, crossSections) : null;
|
||||
const box = own && builds("box_culvert", "box") ? section.box : undefined;
|
||||
if (!layout && !fordLayout && !box && !revetLayout && !paveChute) continue;
|
||||
const chainage = section.chainage_m;
|
||||
const stationFrame: StructureFrame = {
|
||||
|
||||
@@ -12,6 +12,10 @@ 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;
|
||||
|
||||
/** `culvert` 자리 주인 = 박스 · 세월교 흙막이(body) · 독립 기슭막이(관 숨김) · 배관. */
|
||||
export function culvertBody(section: CrossSection): boolean {
|
||||
const spec = section.culvert;
|
||||
|
||||
@@ -142,7 +142,7 @@ def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]
|
||||
place = PLACE_KO.get(row["placement"], row["placement"])
|
||||
owner = OWNER_KO.get(row["owner"], row["owner"])
|
||||
if row["placement"] not in part["자리꼴"] or row["owner"] not in part["정본"]:
|
||||
out.append(f"「{row['name']}」 {branch} 「{name}」 — {place} · {owner} 에 안 맞음")
|
||||
out.append(f"「{row['name']}」 {branch} 「{name}」 — {place} · {owner}에 안 맞음")
|
||||
for key in part["읽는 칸"]:
|
||||
if key in keys and key in hidden:
|
||||
out.append(
|
||||
|
||||
@@ -156,7 +156,7 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
|
||||
const place = PLACE_KO[row.placement] ?? row.placement;
|
||||
const owner = OWNER_KO[row.owner] ?? row.owner;
|
||||
if (!part.자리꼴.includes(row.placement) || !part.정본.includes(row.owner))
|
||||
out.push(`「${row.name}」 ${branch} 「${name}」 — ${place} · ${owner} 에 안 맞음`);
|
||||
out.push(`「${row.name}」 ${branch} 「${name}」 — ${place} · ${owner}에 안 맞음`);
|
||||
for (const key of part["읽는 칸"])
|
||||
if (keys.has(key) && hidden.includes(key))
|
||||
out.push(
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* PLAN 323-9 ④-② — 노선 끝(EP)처럼 같은 m 카드 둘이면 3D 관 · 세월교 · BOX 몸체는 한 번.
|
||||
* 화면 TS(`buildCorridorStructures`)를 그 자리에서 트랜스파일해 Node 로 돌림(로더 = helper_135_3 과 같음).
|
||||
* 입력 = 73-4 픽스처(세월교 173.087 · BOX 352.14) · 결과 = 표준출력 JSON. 짝 = test_323_9_twin_cards_3d.py.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
const STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
three: require(path.join(ROOT, "config", "node_modules", "three")),
|
||||
};
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) {
|
||||
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
cache.set(full, data);
|
||||
return data;
|
||||
}
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const { buildCorridorStructures } = loadTs(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Corridor_Structures.ts"),
|
||||
);
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
||||
);
|
||||
const clone = (v) => JSON.parse(JSON.stringify(v));
|
||||
const at = (c) => clone(fixture.cross_sections.find((s) => Math.abs(s.chainage_m - c) < 0.6));
|
||||
/** 직선 노선 — 가장 가까운 측점 프레임에서 앞쪽으로 편다. */
|
||||
const frameAtOf = (sections) => (atM) => {
|
||||
const s = sections.reduce((b, x) =>
|
||||
Math.abs(x.chainage_m - atM) < Math.abs(b.chainage_m - atM) ? x : b,
|
||||
);
|
||||
const [lx, ly] = s.frame.left_xy;
|
||||
const along = atM - s.chainage_m;
|
||||
return {
|
||||
cx: s.center_x - ly * along,
|
||||
cy: s.center_y + lx * along,
|
||||
leftX: lx,
|
||||
leftY: ly,
|
||||
designZ: s.design?.design_elevation_m ?? null,
|
||||
};
|
||||
};
|
||||
/** 부재 차림표 — 종류 · 열쇠 · 측점(같은 몸체를 두 번 세우면 같은 줄이 둘). */
|
||||
const kinds = (sections) =>
|
||||
buildCorridorStructures(sections, frameAtOf(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 out = {};
|
||||
for (const c of [173.087, 352.14]) {
|
||||
const one = at(c);
|
||||
const twin = kinds([one, clone(one)]);
|
||||
pick.ownsBodies = () => true; // 고치기 전(측점마다 몸체) — 기준이 변경을 재는지
|
||||
const before = kinds([one, clone(one)]);
|
||||
pick.ownsBodies = own;
|
||||
out[c] = { one: kinds([one]), twin, before };
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -127,16 +127,16 @@ def test_check_rules(ts: dict) -> None:
|
||||
"부품 「끔」 — 없는 부품",
|
||||
]
|
||||
assert got["rule218"] == ["「개거(겉도랑)」 형상이 「없음」이면 횡단도도 「안 만듦」"]
|
||||
assert got["mismatch"] == ["「옹벽」 형상 「배관 세트」 — 구간 · 구조물 에 안 맞음"]
|
||||
assert got["mismatch"] == ["「옹벽」 형상 「배관 세트」 — 구간 · 구조물에 안 맞음"]
|
||||
assert got["hide_read"] == [
|
||||
"「배관」 형상 「배관 세트」 이 쓰는 칸 「흙막이 기초」 — 숨길 수 없음"
|
||||
]
|
||||
assert got["hide_no_before"] == [] # 앞 판 없음 = 새로 숨긴 것 없음
|
||||
assert got["fixed"] == [
|
||||
# 표 줄도 함께 걸림 — 자리꼴 · 정본을 바꾸면 그 종류 부품이 안 맞음
|
||||
"「BOX암거」 형상 「BOX 본체 · 날개벽」 — 구간 · 관 지점 에 안 맞음",
|
||||
"「BOX암거」 3D 「BOX 본체 · 날개벽」 — 구간 · 관 지점 에 안 맞음",
|
||||
"「기슭막이」 형상 「독립 기슭막이」 — 구간 · 구조물 에 안 맞음",
|
||||
"「BOX암거」 형상 「BOX 본체 · 날개벽」 — 구간 · 관 지점에 안 맞음",
|
||||
"「BOX암거」 3D 「BOX 본체 · 날개벽」 — 구간 · 관 지점에 안 맞음",
|
||||
"「기슭막이」 형상 「독립 기슭막이」 — 구간 · 구조물에 안 맞음",
|
||||
"「배관」 「관경」 선택지 값 — 고칠 수 없음(이름만 고침)",
|
||||
"「BOX암거」 자리꼴 — 만든 뒤 고정",
|
||||
"「기슭막이」 정본 — 만든 뒤 고정",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""PLAN 323-9 ④-② — 노선 끝(EP)처럼 같은 m 카드 둘이면 3D 관 · 세월교 · BOX 몸체가 둘 서던 흠.
|
||||
|
||||
3D 몸체는 구조물(관) 하나에 한 번 — 같은 m 카드 둘이면 앞 카드만 세움(`ownsBodies`).
|
||||
Node 헬퍼 = `helper_323_9_twin_cards_3d.cjs`(73-4 픽스처 · 진짜 `buildCorridorStructures`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
run = subprocess.run(
|
||||
["node", str(HERE / "helper_323_9_twin_cards_3d.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("at", ["173.087", "352.14"])
|
||||
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"]) # 고치기 전 = 두 번(시험이 변경을 잼)
|
||||
if at == "173.087": # 세월교 관 두 련 — 관 몸체도 한 번
|
||||
assert sum(k.startswith("pipe:") for k in got["twin"]) == 2
|
||||
Reference in New Issue
Block a user