feat(B06): 세월교 · 박스 흙막이 — 본체가 흙막이를 따라감 · 세월교 관 1 m 단위(80)
- 세월교 흙막이 쪽 관 끝 = 흙막이 전면(관 invert 높이) · 바닥판 끝 = 배면 · 라벨 관 길이 - 세월교 관 길이 1 m 단위 — 기본 자리 올림만큼 흙막이를 바깥으로(유출 먼저) · 좌우 걸음 1 m - 박스 흙막이를 바깥으로 밀면 상판 · 저판 · 내공이 배면까지 따라감 - 흙막이 ↺ 가 캐시 사본(design.revet_adjust)까지 비움 — 옮긴 자리가 되살아나던 것 - 시험: test_80_ford_pipe(새) · test_73_4_body_guard 유출 벽 1 m 맞춤 반영 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C99ToP6eDSTba2XZRUumb8
This commit is contained in:
@@ -17,6 +17,7 @@ import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type {
|
||||
CulvertDesignTrim,
|
||||
CulvertLayout,
|
||||
OffsetPoint,
|
||||
WallAdjust,
|
||||
WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
@@ -26,11 +27,13 @@ import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
|
||||
|
||||
type Role = "inlet" | "outlet";
|
||||
|
||||
/** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) 표고 · 흙막이 기본 순수 높이(79-7 · 79b). */
|
||||
/** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) · 관 invert(세월교 바닥판 상면) 표고 ·
|
||||
* 흙막이 기본 순수 높이(79-7 · 79b · 80). */
|
||||
export interface BodyEnd {
|
||||
offset: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
invert: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
@@ -62,6 +65,7 @@ export function bodyGuardBase(
|
||||
offset: side.offset,
|
||||
top: side.topElevation,
|
||||
bottom: side.bottomElevation,
|
||||
invert: side.bottomElevation,
|
||||
// 박스 높이 값 그대로(사용자 원문 2 m → 2 m · 79b)
|
||||
height: layout.box.inner_height_m,
|
||||
}
|
||||
@@ -83,6 +87,7 @@ export function bodyGuardBase(
|
||||
offset: side.pipeEnd.offset,
|
||||
top: side.top.elevation,
|
||||
bottom: side.slabTopElevation - layout.ford.slab_thickness_m,
|
||||
invert: side.slabTopElevation,
|
||||
height: side.top.elevation - (side.slabTopElevation - layout.ford.slab_thickness_m),
|
||||
}
|
||||
: null;
|
||||
@@ -109,7 +114,7 @@ export function bodyGuardAxes(
|
||||
lean: number;
|
||||
thickness: number;
|
||||
},
|
||||
): { x: number; d: number } {
|
||||
): BodyAxes {
|
||||
const d = FILL_SLOPE_RATIO_MIN * (wall.edgeElevation - end.top);
|
||||
// 배면 위치는 자리(offset)에 평행이동만 함 — 0 에 세워 재고 옮김(노면 멈춤도 같게).
|
||||
const probe = buildRevetWallGeometry({
|
||||
@@ -123,7 +128,45 @@ export function bodyGuardAxes(
|
||||
roadElevation: wall.edgeElevation,
|
||||
});
|
||||
const anchorOffset = end.offset - probe.bottomBack.offset;
|
||||
return { x: (anchorOffset - wall.autoOffset) * wall.outward - d, d };
|
||||
// 배면 → 관 invert 높이 전면 — 세월교 관이 흙막이 안으로 드는 길이(80)
|
||||
const front =
|
||||
probe.points[2].offset + wall.outward * probe.lean * (probe.topJoint.elevation - end.invert);
|
||||
const through = (front - probe.bottomBack.offset) * wall.outward;
|
||||
return {
|
||||
x: (anchorOffset - wall.autoOffset) * wall.outward - d,
|
||||
d,
|
||||
through,
|
||||
outward: wall.outward,
|
||||
};
|
||||
}
|
||||
|
||||
export type BodyAxes = { x: number; d: number; through: number; outward: number };
|
||||
|
||||
/** 세월교 관은 1 m 단위(80) — 흙막이 전면까지 든 관 길이를 올림하고 모자란 만큼 흙막이를 바깥으로
|
||||
* 미는 기본 x(유출 먼저 · 배관 정수 맞춤과 같은 쪽). 손으로 민 x 는 그 위에 1 m 걸음으로 얹힘. */
|
||||
export function fordPipeFit(
|
||||
base: BodyGuardBase | null,
|
||||
culvert: { body?: string | null; inlet: { structure?: string }; outlet: { structure?: string } },
|
||||
axesOf: (role: Role) => BodyAxes | null,
|
||||
): [BodyAxes | null, BodyAxes | null] {
|
||||
const axes: [BodyAxes | null, BodyAxes | null] = [axesOf("inlet"), axesOf("outlet")];
|
||||
if (!base || culvert.body !== "ford") return axes;
|
||||
const roles = ["inlet", "outlet"] as const;
|
||||
const guarded = roles.map((role, i) => (culvert[role].structure ? axes[i] : null));
|
||||
const tips = roles.map((role, i) => {
|
||||
const end = base.ends[role];
|
||||
const reach = guarded[i] ? guarded[i]!.outward * guarded[i]!.through : 0;
|
||||
return end && { offset: end.offset + reach, z: end.invert };
|
||||
});
|
||||
if (!tips[0] || !tips[1]) return axes;
|
||||
// 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 fit = guarded[at];
|
||||
if (!fit || gap < 1e-6) return axes;
|
||||
axes[at] = { ...fit, x: fit.x + gap };
|
||||
return axes;
|
||||
}
|
||||
|
||||
/** 벽이 선 쪽은 흙막이 트림 · 안 선 쪽은 본체 트림(73-4). 벽이 하나도 없으면 null. */
|
||||
@@ -166,17 +209,61 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
|
||||
): T | null {
|
||||
if (!layout || !culvert?.culvert.body) return layout;
|
||||
const guarded = (outward: number): boolean => culvert.walls.some((w) => w.outward === outward);
|
||||
const mid = layout.label.at.offset;
|
||||
const reach = (points: OffsetPoint[], outward: number, offset: number): OffsetPoint[] =>
|
||||
points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset } : p));
|
||||
if ("box" in layout && "topSlab" in layout) {
|
||||
const sides = layout.sides.map((side) =>
|
||||
guarded(side.role === "left" ? 1 : -1)
|
||||
? { ...side, fillLine: [], fillSegments: [], cutLine: null }
|
||||
: side,
|
||||
);
|
||||
return { ...layout, sides };
|
||||
// 박스 본체가 흙막이 배면까지 따라감 — 흙막이를 바깥으로 민 만큼만(80)
|
||||
let { topSlab, bottomSlab, barrel } = layout;
|
||||
for (const side of layout.sides) {
|
||||
const outward = side.role === "left" ? 1 : -1;
|
||||
const wall = culvert.walls.find((w) => w.role !== "extra" && w.outward === outward);
|
||||
const to = wall?.bottomBack.offset ?? side.offset;
|
||||
if ((to - side.offset) * outward < 1e-6) continue;
|
||||
[topSlab, bottomSlab, barrel] = [topSlab, bottomSlab, barrel].map((p) =>
|
||||
reach(p, outward, to),
|
||||
);
|
||||
}
|
||||
const run =
|
||||
Math.max(...topSlab.map((p) => p.offset)) - Math.min(...topSlab.map((p) => p.offset));
|
||||
return { ...layout, sides, topSlab, bottomSlab, barrel, bodyLengthM: run };
|
||||
}
|
||||
const ford = layout as FordLayout;
|
||||
const sides = ford.sides.map((side) =>
|
||||
guarded(side.outward) ? { ...side, parts: [], fillSegments: [], cutLine: null } : side,
|
||||
);
|
||||
return { ...ford, sides } as T;
|
||||
// 관 · 바닥판이 흙막이까지 따라감 — 관 끝 = 흙막이 전면(관 invert 높이) · 바닥판 끝 = 배면(80)
|
||||
let { pipe, slabBridge } = ford;
|
||||
for (const side of ford.sides) {
|
||||
const wall = culvert.walls.find((w) => w.role === side.role && w.outward === side.outward);
|
||||
if (!wall) continue;
|
||||
const front =
|
||||
wall.points[2].offset +
|
||||
wall.outward * wall.lean * (wall.topJoint.elevation - side.slabTopElevation);
|
||||
pipe = {
|
||||
outer: reach(pipe.outer, side.outward, front),
|
||||
inner: reach(pipe.inner, side.outward, front),
|
||||
};
|
||||
slabBridge = reach(slabBridge, side.outward, wall.bottomBack.offset);
|
||||
}
|
||||
if (pipe === ford.pipe) return { ...ford, sides } as T;
|
||||
const [right, left] = pipe.outer.slice(2);
|
||||
const lengthM = Math.hypot(left.offset - right.offset, left.elevation - right.elevation);
|
||||
const slabOffsets = [
|
||||
...slabBridge,
|
||||
...sides.flatMap((s) => s.parts.flatMap((p) => p.points)),
|
||||
].map((p) => p.offset);
|
||||
return {
|
||||
...ford,
|
||||
sides,
|
||||
pipe,
|
||||
slabBridge,
|
||||
slabLengthM: Math.max(...slabOffsets) - Math.min(...slabOffsets),
|
||||
label: { ...ford.label, text: `${ford.label.text} L${Math.ceil(lengthM - 1e-6)}m` },
|
||||
} as T;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
bodyGuardAxes,
|
||||
bodyGuardBase,
|
||||
bodyGuardHeight,
|
||||
fordPipeFit,
|
||||
mergeGuardTrim,
|
||||
} from "./B06_Section_UI_Cross_Body_Guard";
|
||||
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
@@ -169,9 +170,10 @@ export function computeCulvertLayout(
|
||||
const inletWallSpec = pipeWallSpec(culvert.inlet, adjInlet);
|
||||
const outletWallSpec = pipeWallSpec(culvert.outlet, adjOutlet);
|
||||
// 본체 끝 자리(x 0점 · d 기본값). 본체 없으면(연동 옆 측점) 배관 규칙.
|
||||
const bodyAxesOf = (role: "inlet" | "outlet", info: typeof inletInfo) => {
|
||||
const bodyAxesOf = (role: "inlet" | "outlet") => {
|
||||
const end = body?.ends[role];
|
||||
const spec = role === "inlet" ? inletWallSpec : outletWallSpec;
|
||||
const [spec, info] =
|
||||
role === "inlet" ? [inletWallSpec, inletInfo] : [outletWallSpec, outletInfo];
|
||||
if (!end) return null;
|
||||
return bodyGuardAxes(end, {
|
||||
autoOffset: slopeZeroAnchor(info.edge, info.outward, spec.height, spec.lean),
|
||||
@@ -182,8 +184,7 @@ export function computeCulvertLayout(
|
||||
thickness: REVET_THICKNESS_M,
|
||||
});
|
||||
};
|
||||
const inletBody = bodyAxesOf("inlet", inletInfo);
|
||||
const outletBody = bodyAxesOf("outlet", outletInfo);
|
||||
const [inletBody, outletBody] = fordPipeFit(body, culvert, bodyAxesOf);
|
||||
const onBody = (adjust: WallAdjust, axes: { x: number; d: number } | null): WallAdjust =>
|
||||
axes ? { ...adjust, x: axes.x + adjust.x, d: adjust.d ?? axes.d } : adjust;
|
||||
const minX = culvert.body ? Number.NEGATIVE_INFINITY : undefined;
|
||||
|
||||
@@ -647,6 +647,7 @@ export function createCrossSectionCard(
|
||||
inletIsBasin: () => culvertInletIsBasin,
|
||||
hiddenPipe: () => culvertHiddenPipe,
|
||||
bodyGuard: () => !!(section.culvert ?? culvertLink?.source.culvert)?.body,
|
||||
fordGuard: () => (section.culvert ?? culvertLink?.source.culvert)?.body === "ford",
|
||||
inletOptions: () => culvertInletOptions,
|
||||
extraState: () => culvertExtraState,
|
||||
basinExtraState: () => culvertBasinExtraState,
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface StructurePanelContext {
|
||||
hiddenPipe: () => boolean;
|
||||
/** 세월교 · BOX암거 흙막이 세트인가(링크 카드는 소유 측점 세트 · 79b). */
|
||||
bodyGuard?: () => boolean;
|
||||
/** 세월교 흙막이 — 안이 관이라 관 보호공처럼 1 m 걸음(80). */
|
||||
fordGuard?: () => boolean;
|
||||
inletOptions: () => { revetAllowed: boolean; basinLUAllowed: boolean };
|
||||
extraState: () => { canAdd: boolean; count: number };
|
||||
basinExtraState: () => { canAdd: boolean; count: number };
|
||||
@@ -144,8 +146,9 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
|
||||
// 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다(추가 벽은 항상 이동).
|
||||
canNudge: (key) => key !== "inlet" || !ctx.inletIsBasin(),
|
||||
// 걸음 분류(2026-08-29 사용자): **배관 기준벽만 1m**, 나머지(독립 기슭막이 기준벽·
|
||||
// 배관/독립 종속 추가 벽·옛 D경로 독립 벽)는 0.1m.
|
||||
fineMove: (key) => (key !== "inlet" && key !== "outlet") || ctx.hiddenPipe(),
|
||||
// 배관/독립 종속 추가 벽·옛 D경로 독립 벽)는 0.1m. 세월교 흙막이는 안이 관이라 1m(80).
|
||||
fineMove: (key) =>
|
||||
(key !== "inlet" && key !== "outlet") || (ctx.hiddenPipe() && !ctx.fordGuard?.()),
|
||||
extraState: () => ctx.extraState(),
|
||||
isLinkedCard: () => ctx.isLinked,
|
||||
basinExtraState: () => ctx.basinExtraState(),
|
||||
|
||||
@@ -293,8 +293,8 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
refreshWithLinked(chainageM);
|
||||
},
|
||||
reset: (chainageM, role) => {
|
||||
revetShifts.delete(revetKey(chainageM, role));
|
||||
persistRevetShifts();
|
||||
// 캐시 사본(design.revet_adjust)까지 비움 — 남기면 adjustFor 가 옮긴 자리로 되살림(80)
|
||||
storeAdjust(revetKey(chainageM, role), { ...ZERO_ADJUST });
|
||||
refreshWithLinked(chainageM);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 상시 계획서
|
||||
|
||||
**남은 작업만 두는 유일한 계획서.** 목록이 비면 프로젝트 최종 완료.
|
||||
|
||||
## 80. 세월교 · 박스암거 흙막이 — 본체가 흙막이를 따라감
|
||||
|
||||
사용자 확정(10-8) — 추천 노선 현재 유지 · 횡단 보기 현재 유지 · 세월교 안은 배관(1 m 기준) · 흙막이면 관이 흙막이 안까지 · 좌우 이동에 본체가 따라감 · 박스암거 다시 확인.
|
||||
|
||||
- [x] 80-1 세월교 흙막이 쪽 관 끝 = 흙막이 전면(관 invert 높이) · 바닥판 끝 = 흙막이 배면 · 라벨에 관 길이(L ○m)
|
||||
- [x] 80-2 세월교 관 길이 1 m 단위 — 기본 자리에서 올림만큼 흙막이를 바깥으로(유출 먼저) · 세월교 흙막이 좌우 걸음 1 m
|
||||
- [x] 80-3 박스 흙막이를 바깥으로 밀면 박스 본체(상판 · 저판 · 내공)가 흙막이 배면까지 따라감
|
||||
- [x] 80-4 흙막이 ↺(자동 자리로)가 캐시 사본까지 비움 — 옮긴 자리가 되살아나던 것
|
||||
- 검증완료 — test_80_ford_pipe 5 · 관련 40 통과 · typecheck · 화면 검증_랩탑메인 세월교 9+15 캐시(L6 → ▶ 두 번 L8 · 양쪽 흙막이 L7 · ↺ 복귀) · 검증_sub7 8+15 캐시 박스(▶ 1 m 본체 따라감) · 캐시 지움 · 저장 안 함
|
||||
@@ -0,0 +1,120 @@
|
||||
/* 80 세월교 흙막이 관 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림.
|
||||
* 입력 = 73-4 픽스처(세월교 173.09 · 박스 352.14 유입 · 유출 흙막이) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_80_ford_pipe.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 STUBS = {
|
||||
"@ui/ui_template_toast": { showToast: () => undefined },
|
||||
};
|
||||
|
||||
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);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", f);
|
||||
const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts"));
|
||||
|
||||
const fixture = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"),
|
||||
);
|
||||
const sections = fixture.cross_sections;
|
||||
const FORD = sections.find((s) => Math.abs(s.chainage_m - 173.087) < 0.6);
|
||||
const clone = (v) => JSON.parse(JSON.stringify(v));
|
||||
const r3 = (v) => Math.round(v * 1000) / 1000;
|
||||
|
||||
/** 관 길이 · 쪽마다 관 끝 ↔ 흙막이 전면(관 invert 높이) · 바닥판 끝 ↔ 흙막이 배면. */
|
||||
function pipeFit(edit) {
|
||||
const section = clone(FORD);
|
||||
edit(section);
|
||||
const list = sections.map((s) => (s === FORD ? section : s));
|
||||
const { ford, culvert } = computeStoredLayouts(section, list);
|
||||
const [right, left] = ford.pipe.outer.slice(2);
|
||||
const ends = { 1: left.offset, [-1]: right.offset };
|
||||
const slab = ford.slabBridge.map((p) => p.offset);
|
||||
const slabEnd = { 1: Math.max(...slab), [-1]: Math.min(...slab) };
|
||||
return {
|
||||
lengthM: r3(Math.hypot(left.offset - right.offset, left.elevation - right.elevation)),
|
||||
label: ford.label.text,
|
||||
sides: ford.sides.map((side) => {
|
||||
const wall = culvert.walls.find((w) => w.role === side.role && w.outward === side.outward);
|
||||
if (!wall) return { role: side.role, guard: false };
|
||||
const front =
|
||||
wall.points[2].offset +
|
||||
wall.outward * wall.lean * (wall.topJoint.elevation - side.slabTopElevation);
|
||||
return {
|
||||
role: side.role,
|
||||
guard: true,
|
||||
pipeGap: r3(ends[side.outward] - front),
|
||||
slabGap: r3(slabEnd[side.outward] - wall.bottomBack.offset),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const move = (role, x) => (s) => {
|
||||
s.design.revet_adjust = { ...(s.design.revet_adjust ?? {}), [role]: { x, d: null, h: null } };
|
||||
};
|
||||
const out = {
|
||||
both: pipeFit(() => undefined),
|
||||
inletPlus1: pipeFit(move("inlet", 1)),
|
||||
outletPlus2: pipeFit(move("outlet", 2)),
|
||||
inletOnly: pipeFit((s) => {
|
||||
s.culvert.outlet = { role: "outlet", structure: "" };
|
||||
}),
|
||||
};
|
||||
|
||||
// 박스 352.14 — 흙막이를 바깥으로 밀면 본체 끝이 배면까지 따라감 · 안 밀면 그대로
|
||||
const BOX = sections.find((s) => Math.abs(s.chainage_m - 352.14) < 0.6);
|
||||
function boxFit(edit) {
|
||||
const section = clone(BOX);
|
||||
edit(section);
|
||||
const { box, culvert } = computeStoredLayouts(
|
||||
section,
|
||||
sections.map((s) => (s === BOX ? section : s)),
|
||||
);
|
||||
const top = box.topSlab.map((p) => p.offset);
|
||||
return {
|
||||
bodyLengthM: r3(box.bodyLengthM),
|
||||
gaps: culvert.walls
|
||||
.filter((w) => w.role !== "extra")
|
||||
.map((w) => r3((w.outward > 0 ? Math.max(...top) : Math.min(...top)) - w.bottomBack.offset)),
|
||||
};
|
||||
}
|
||||
out.boxAuto = boxFit(() => undefined);
|
||||
out.boxInletPlus1 = boxFit(move("inlet", 1));
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -52,7 +52,8 @@ def test_박스를_늘리면_벽도_m_단위로_따라감(screen: dict) -> None:
|
||||
|
||||
def test_세월교_벽_배면이_관_끝(screen: dict) -> None:
|
||||
walls = {w["role"]: w for w in screen["ford"]["walls"]}
|
||||
assert walls["outlet"]["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
# 유출은 관 1 m 맞춤만큼 바깥으로 물러남(80 · test_80_ford_pipe) — 1 m 안
|
||||
assert abs(walls["outlet"]["dOffset"]) < 1
|
||||
assert walls["outlet"]["dElevation"] == pytest.approx(0, abs=1e-3)
|
||||
assert walls["inlet"]["dOffset"] == pytest.approx(0, abs=1e-3)
|
||||
# 윗면 = 측벽 상단(상판 높이) — 노면 위로는 안 솟음(79-7)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""80 세월교 · 박스 흙막이 — 세월교 안은 관이라 1 m 단위 · 관이 흙막이 안까지 · 흙막이를 옮기면 본체가 따라감.
|
||||
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_80_ford_pipe.cjs`) · 픽스처 = 73-4 세월교 173.09 · 박스 352.14.
|
||||
"""
|
||||
|
||||
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))
|
||||
|
||||
HELPER = Path(__file__).parent / "helper_80_ford_pipe.cjs"
|
||||
NODE_MODULES = ROOT / "config" / "node_modules"
|
||||
|
||||
pytestmark = pytest.mark.skipif(not NODE_MODULES.is_dir(), reason="프론트엔드 의존성 미설치")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def fit() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def _whole(length: float) -> bool:
|
||||
return abs(length - round(length)) < 1e-3
|
||||
|
||||
|
||||
def test_기본_자리_관_길이는_1m_단위(fit: dict) -> None:
|
||||
for case in ("both", "inletOnly"):
|
||||
assert _whole(fit[case]["lengthM"]), fit[case]
|
||||
assert fit[case]["label"].endswith(f"L{round(fit[case]['lengthM'])}m")
|
||||
|
||||
|
||||
def test_관은_흙막이_전면까지_바닥판은_배면까지(fit: dict) -> None:
|
||||
for case in (fit[k] for k in ("both", "inletPlus1", "outletPlus2", "inletOnly")):
|
||||
for side in case["sides"]:
|
||||
if side["guard"]:
|
||||
assert side["pipeGap"] == pytest.approx(0, abs=1e-3)
|
||||
assert side["slabGap"] == pytest.approx(0, abs=1e-3)
|
||||
|
||||
|
||||
def test_흙막이를_옮기면_관이_그만큼_따라감(fit: dict) -> None:
|
||||
base = fit["both"]["lengthM"]
|
||||
assert fit["inletPlus1"]["lengthM"] == pytest.approx(base + 1, abs=1e-3)
|
||||
assert fit["outletPlus2"]["lengthM"] == pytest.approx(base + 2, abs=1e-3)
|
||||
|
||||
|
||||
def test_흙막이_없는_쪽은_그대로(fit: dict) -> None:
|
||||
assert [s["guard"] for s in fit["inletOnly"]["sides"]] == [True, False]
|
||||
|
||||
|
||||
def test_박스도_흙막이를_밀면_본체가_따라감(fit: dict) -> None:
|
||||
assert fit["boxAuto"]["gaps"] == [0, 0]
|
||||
assert fit["boxInletPlus1"]["gaps"] == [0, 0]
|
||||
assert fit["boxInletPlus1"]["bodyLengthM"] == pytest.approx(
|
||||
fit["boxAuto"]["bodyLengthM"] + 1, abs=1e-3
|
||||
)
|
||||
Reference in New Issue
Block a user