feat(B06): 노견 넓힌 쪽 관 길이 자리 = 사면 우선 + 유출 벽 높이 역산 시험 · 세월교 흙막이 맞춤은 안 넓힌 쪽만 밂(139-6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -145,7 +145,9 @@ export type BodyAxes = { x: number; d: number; through: number; outward: number
|
||||
/** 세월교 관은 1 m 단위(80) — 흙막이 전면까지 든 관 길이를 올림하고 모자란 만큼 흙막이를 바깥으로
|
||||
* 미는 기본 x(유출 먼저 · 배관 정수 맞춤과 같은 쪽). 손으로 민 x 는 그 위에 1 m 걸음으로 얹힘.
|
||||
* 맞춤 길이는 **기본 자리**(측벽 조작 0)로만 잼 — 한쪽 이동이 반대쪽 흙막이를 안 밈(107-1 ·
|
||||
* 수량 L 은 그린 관 길이 올림 그대로). */
|
||||
* 수량 L 은 그린 관 길이 올림 그대로). 노견을 넓힌 쪽(139-6)은 사면 우선 — 그 쪽 흙막이는 안 밀고
|
||||
* 반대쪽(안 넓힌 쪽)을 밂 · 둘 다 넓혔으면 표기만 올림.
|
||||
* ponytail: 흙막이 높이 역산 없음 — 윗면이 본체 끝 윗면에 묶임(79-7) · 필요하면 배관 `_Culvert_Fit` 꼴로. */
|
||||
export function fordPipeFit(
|
||||
section: CrossSection,
|
||||
samples: SectionSample[],
|
||||
@@ -167,7 +169,12 @@ export function fordPipeFit(
|
||||
// ponytail: 관 기울기는 작아 바깥 수평 이동으로 길이를 채움
|
||||
const length = Math.hypot(tips[0].offset - tips[1].offset, tips[0].z - tips[1].z);
|
||||
const gap = Math.ceil(length - 1e-6) - length;
|
||||
const at = guarded[1] ? 1 : 0;
|
||||
const shifted = roles.map((role) => {
|
||||
const left = (base.ends[role]?.offset ?? 0) > 0;
|
||||
return (section.design?.[`shoulder_extend_${left ? "left" : "right"}_m`] ?? 0) > 1e-9;
|
||||
});
|
||||
const at = [1, 0].find((i) => guarded[i] && !shifted[i]);
|
||||
if (at === undefined) return axes;
|
||||
const fit = axes[at];
|
||||
if (!guarded[at] || !fit || gap < 1e-6) return axes;
|
||||
axes[at] = { ...fit, x: fit.x + gap };
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/* 139-6 노견을 넓힌 유출 쪽 관 길이 맞춤 헬퍼 — 설계선 엔진(`common_util_cross_design`)으로 노견 확장
|
||||
* 설계를 내고 배수관 기하(`_Cross_Culvert_Geom`)를 돌림. 입력 = B07 픽스처 배관 측점(85.052 · 유출 좌).
|
||||
* 표준단면 수치 = 인자 JSON 파일(파이썬 config 그대로 · 거울 시험과 같은 방식).
|
||||
* 짝 = `test_139_6_pipe_fit.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
const noop = new Proxy({}, { get: () => () => "" });
|
||||
globalThis.document = { addEventListener: () => undefined };
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) return JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const resolve = (base) =>
|
||||
fs.existsSync(base) && fs.statSync(base).isFile() ? base : `${base}.ts`;
|
||||
const req = (r) => {
|
||||
if (r.startsWith("@ui/") || r.startsWith("@config/")) return noop;
|
||||
if (r.startsWith("@util/")) return loadTs(resolve(path.join(ROOT, "common_util", r.slice(6))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), r)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const { computeCrossDesign } = loadTs(
|
||||
path.join(ROOT, "common_util", "common_util_cross_design.ts"),
|
||||
);
|
||||
const { computeCulvertLayout, REVET_EMBED_DEPTH_M } = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Geom.ts"),
|
||||
);
|
||||
const { cutLength } = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Solve.ts"),
|
||||
);
|
||||
|
||||
const sections = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"),
|
||||
).cross_sections;
|
||||
const section = sections.find((s) => s.culvert && !s.culvert.hidden_pipe);
|
||||
const d = section.design;
|
||||
const standard = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const designOf = (extendLeft) =>
|
||||
computeCrossDesign(section.samples, d.design_elevation_m, {
|
||||
groundType: d.ground_type,
|
||||
sectionMode: d.section_mode,
|
||||
ditchSide: d.ditch_side,
|
||||
ditchType: d.ditch_type ?? "standard",
|
||||
paved: !!d.paved,
|
||||
standard,
|
||||
shoulderExtendLeftM: extendLeft,
|
||||
});
|
||||
const r3 = (v) => Math.round(v * 1000) / 1000;
|
||||
const run = (extendLeft) => {
|
||||
const design = designOf(extendLeft);
|
||||
const s = { ...section, design };
|
||||
const layout = computeCulvertLayout(s, section.samples, undefined, "L");
|
||||
const wall = layout.walls.find((w) => w.role === "outlet");
|
||||
const edge = design.road_edges.left;
|
||||
return {
|
||||
edge: r3(edge.offset_m),
|
||||
extend: design.shoulder_extend_left_m ?? 0,
|
||||
pureHeight: r3(wall.height + REVET_EMBED_DEPTH_M),
|
||||
lengthM: layout.pipe.lengthM,
|
||||
cut: r3(cutLength(layout.pipeCorners)),
|
||||
slope: r3(layout.fillSlope.ratio),
|
||||
};
|
||||
};
|
||||
process.stdout.write(JSON.stringify({ base: run(0), shifted: run(1.0), shifted2: run(1.3) }));
|
||||
@@ -0,0 +1,60 @@
|
||||
"""139-6 노견을 넓힌 유출 쪽 관 길이 자리 — 사면 우선(물매 1:1.2 그대로) + 유출 벽 높이 역산(0.1 m).
|
||||
|
||||
설계선 엔진으로 노견 확장 설계를 내고 배수관 기하를 돌린 값(`helper_139_6_pipe_fit.cjs` · B07 픽스처
|
||||
배관 85.052 · 유출 = 좌). 안 넓힌 측점은 종전 맞춤 그대로(나눔 전 코드와 같은 값 · 5.772 → 표기 6).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
|
||||
HELPER = Path(__file__).parent / "helper_139_6_pipe_fit.cjs"
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory) -> dict:
|
||||
standard = tmp_path_factory.mktemp("std") / "standard.json"
|
||||
standard.write_text(json.dumps(STANDARD_CROSS_SECTION), encoding="utf-8")
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER), str(standard)],
|
||||
cwd=str(ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_안_넓힌_측점은_종전_그대로(out: dict) -> None:
|
||||
base = out["base"]
|
||||
assert (base["edge"], base["pureHeight"], base["lengthM"]) == (2, 2, 6)
|
||||
assert base["cut"] == pytest.approx(5.772)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, extend", [("shifted", 1.0), ("shifted2", 1.3)])
|
||||
def test_넓힌_쪽은_물매_그대로_높이_역산(out: dict, name: str, extend: float) -> None:
|
||||
case = out[name]
|
||||
assert case["edge"] == pytest.approx(2 + extend) and case["extend"] == extend
|
||||
assert case["slope"] == pytest.approx(1.2, abs=0.05) # 사면 우선 — 물매 그대로
|
||||
assert round(case["pureHeight"] * 10) == pytest.approx(case["pureHeight"] * 10) # 0.1 m 눈금
|
||||
assert case["lengthM"] - case["cut"] < 0.1 # 눈금 탓 남는 차만 표기 올림
|
||||
assert float(case["lengthM"]).is_integer()
|
||||
|
||||
|
||||
def test_높이가_실제로_바뀜(out: dict) -> None:
|
||||
"""대조 — 1.3 m 넓히면 유출 벽 높이가 2.0 → 1.8 로 역산됨(시험이 높이 길을 실제로 잼)."""
|
||||
assert out["shifted2"]["pureHeight"] == pytest.approx(1.8)
|
||||
Reference in New Issue
Block a user