Files
Aislo/resources/tester/helper_135_3_chute_solid.cjs
T
eomsangdonandClaude Opus 5.5 281c71b55b refactor(logic): 348-1 순수 계산 TS 49 파일 <페이지>/logic/ 로 옮김 — 이름 앞머리 그대로 · 동작 고침 없음
- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로
- 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판)
- import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/
- 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob
- design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄)
- 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-16 13:44:50 +09:00

271 lines
10 KiB
JavaScript

/* PLAN 135-3 — B05 3D 도수로 · 돌붙임 띠 솔리드 헬퍼. 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림.
* 입력 = 73-4 픽스처(박스 352.14 · 세월교 173.087 · 일반 180) · 결과 = 표준출력 JSON. 짝 = test_135_3_chute_solid.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 B05 = (f) => path.join(ROOT, "B05_Profile", `${f}.ts`);
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const { buildCorridorStructures } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
const { structureHashParts } = loadTs(B05("logic/B05_Profile_UI_Corridor_Structures_Hash"));
const { structureTopLoops } = loadTs(B05("logic/B05_Profile_UI_Corridor_Tin_Design"));
const { chuteBandOutline, isChuteSegment } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom"));
const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford"));
const { outletFillLine } = loadTs(B06("B06_Section_UI_Cross_Ford_Pavement"));
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 CHUTE = { structure: "돌붙임", thickness_m: 0.3 };
/** 원지반을 내려 유출 쪽이 성토가 되게 한다(픽스처는 절토 측점). */
const lower = (s, dz) => {
s.samples.forEach((p) => {
p.z -= dz;
p.elevation_m -= dz;
});
return s;
};
/** 직선 노선 — 가장 가까운 측점 프레임에서 앞쪽으로 편다. */
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 chutesOf = (sections) =>
buildCorridorStructures(sections, frameAtOf(sections)).filter((s) => s.kind === "chute");
const lengthOf = (solid) => {
const a = solid.rings[0];
const b = solid.rings[solid.rings.length - 1];
return Math.round(Math.hypot(b.cx - a.cx, b.cy - a.cy) * 1000) / 1000;
};
const flat = (pts) => pts.map((p) => [p.offset, p.elevation]);
const same = (a, b) =>
a.length === b.length &&
a.every(([o, e], i) => Math.abs(o - b[i][0]) + Math.abs(e - b[i][1]) < 1e-9);
const out = {};
function check(name, section, expectedBands, expectedLength, strip) {
const without = clone(section);
strip(without);
const withChute = section;
const solids = chutesOf([withChute]);
const all = buildCorridorStructures([withChute], frameAtOf([withChute]));
const allWithout = buildCorridorStructures([without], frameAtOf([without]));
out[name] = {
count: solids.length,
expected: expectedBands.length,
sameShape: solids.every((s, i) => expectedBands[i] && same(s.polygon, flat(expectedBands[i]))),
keys: [...new Set(solids.map((s) => s.key))],
lengths: solids.map(lengthOf),
expectedLength,
holesSame: structureTopLoops(all).length === structureTopLoops(allWithout).length,
otherSolidsSame: all.length - solids.length === allWithout.length,
};
}
// ① BOX(흙막이 없는) — 유출 날개 쪽 성토 조각 · 길이 = 구체 폭(span_m)
{
const s = lower(at(352.14), 15);
delete s.culvert;
const plain = clone(s);
s.box.chute = CHUTE;
const layout = computeBoxLayout(s, s.samples);
const bands = layout.sides
.filter((side) => side.wingRole === "outlet")
.flatMap((side) => side.fillSegments.filter((g) => g.kind !== "cut"))
.map((g) => chuteBandOutline(g.points, 0.3)?.outline)
.filter(Boolean);
// 해시 — 고르면 바뀌고 · 안 고르면 그대로(빈 칸을 안 붙임)
out.hash = {
changed: structureHashParts(s).join("|") !== structureHashParts(plain).join("|"),
plainNoChute: !structureHashParts(plain).includes("ch"),
};
// 비교용 원본은 chute 없는 꼴로
check("box", s, bands, s.box.span_m, (x) => delete x.box.chute);
out.box.noChuteNoSolid = chutesOf([plain]).length === 0;
}
// ② 세월교(흙막이 없는) — 유출 측벽 성토 조각 · 길이 = 월류 폭(span_m)
{
const s = lower(at(173.087), 15);
delete s.culvert;
s.ford.chute = CHUTE;
const stored = s.design?.ford_adjust;
const layout = computeFordLayout(s, s.samples, {
inlet: { heightM: null, lateralM: 0, slopeM: 0, ...(stored?.inlet ?? {}) },
outlet: { heightM: null, lateralM: 0, slopeM: 0, ...(stored?.outlet ?? {}) },
});
const bands = layout.sides
.filter((side) => side.role === "outlet")
.flatMap((side) => side.fillSegments.filter((g) => g.kind !== "cut"))
.map((g) => chuteBandOutline(g.points, 0.3)?.outline)
.filter(Boolean);
check("ford", s, bands, Math.max(s.ford.span_m, 0.5), (x) => delete x.ford.chute);
}
// ③ 물넘이 포장(136-2 ford_chute) — 유출측 성토사면 선 · 길이 = 포장 폭 · 주인 측점 하나만
{
const s = at(180);
s.ford_pavement = { chainage_m: 180, span_m: 4, depth_m: 0.3, slope_pct: null, chute: CHUTE };
const band = chuteBandOutline(
outletFillLine(s.design).map((p) => p),
0.3,
)?.outline;
check("pave", s, band ? [band] : [], 4, (x) => delete x.ford_pavement.chute);
const neighbour = at(160);
neighbour.ford_pavement = clone(s.ford_pavement);
const pair = [neighbour, s];
out.pave.ownerOnly = chutesOf(pair).length === out.pave.count;
}
// ④ 관(배수관 세트) — 유출 성토 조각(단 사이 포함) · 길이 = 유출 벽 길이(전 5 + 후 5)
{
const s = lower(at(352.14), 15);
delete s.box;
delete s.culvert.body;
s.culvert.hidden_pipe = false;
s.culvert.chute = CHUTE;
const { culvertLayoutOf } = loadTs(B05("B05_Profile_UI_Corridor_Structures"));
const layout = culvertLayoutOf(s);
const bands = (layout?.outletFill.segments ?? [])
.filter(isChuteSegment) // 묻힌 끝 3도 절토 조각도(152-3)
.map((g) => chuteBandOutline(g.points, 0.3)?.outline)
.filter(Boolean);
const spec = s.culvert.outlet;
check(
"pipe",
s,
bands,
(spec.revet_before_m ?? 5) + (spec.revet_after_m ?? 5),
(x) => delete x.culvert.chute,
);
}
// ⑥ 3D 메시 — 띠 끝 마구리 = 마디마다 사다리꼴(부채꼴 아님) · 색 있음
{
const s = lower(at(352.14), 15);
delete s.box;
delete s.culvert.body;
s.culvert.hidden_pipe = false;
s.culvert.chute = CHUTE;
const solid = chutesOf([s])[0];
// 꺾인 띠(점 셋) 로 갈아 끼워 마구리 삼각형 수를 센다
solid.polygon = chuteBandOutline(
[
{ offset: -6, elevation: 832 },
{ offset: -9, elevation: 830 },
{ offset: -12, elevation: 830 },
],
0.3,
).outline.map((p) => [p.offset, p.elevation]);
const { createCorridorGroup } = loadTs(B05("B05_Profile_UI_Corridor_Mesh"));
const group = createCorridorGroup(
{ ribbons: [], outline: { chainages: [], left: [], right: [] }, caps: [], structures: [solid] },
{
x: [s.center_x - 100, s.center_x + 100],
y: [s.center_y - 100, s.center_y + 100],
z: [800, 900],
},
);
const mesh = group.children.find((c) => c.name.startsWith("corridor-structure:chute"));
const count = solid.polygon.length;
const rings = solid.rings.length;
// 마구리 삼각형 넓이 합(부호 없음) = 띠 넓이 × 2 — 부채꼴이면 오목한 자리에서 넘침
const pos = mesh.geometry.getAttribute("position");
const capTris = 4 * (count / 2 - 1);
let capArea = 0;
for (let t = 0; t < capTris; t += 1) {
const v = [0, 1, 2].map((k) => [pos.getX(t * 3 + k), pos.getY(t * 3 + k), pos.getZ(t * 3 + k)]);
const u = v[1].map((x, k) => x - v[0][k]);
const w = v[2].map((x, k) => x - v[0][k]);
const c = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
capArea += Math.hypot(...c) / 2;
}
let bandArea = 0;
solid.polygon.forEach(([x1, y1], i) => {
const [x2, y2] = solid.polygon[(i + 1) % count];
bandArea += x1 * y2 - x2 * y1;
});
out.mesh = {
capAreaRatio: Math.round((capArea / Math.abs(bandArea)) * 1000) / 1000,
found: Boolean(mesh),
colored: Boolean(mesh?.material?.color),
triangles: mesh ? mesh.geometry.getAttribute("position").count / 3 : 0,
expected: 4 * (count / 2 - 1) + 2 * count * (rings - 1),
};
}
// ⑤ ③ 구멍 — 띠 윗면이 평평한 자리(단 사이 수평)여도 구멍 고리를 안 냄 · 같은 꼴 벽이면 냄
{
const s = lower(at(352.14), 15);
delete s.culvert;
s.box.chute = CHUTE;
const flatBand = chuteBandOutline(
[
{ offset: -8, elevation: 830 },
{ offset: -12, elevation: 830 },
],
0.3,
).outline.map((p) => [p.offset, p.elevation]);
const flat = chutesOf([s]).map((x) => ({ ...x, polygon: flatBand }));
out.flatTop = {
asWall: structureTopLoops(flat.map((x) => ({ ...x, kind: "revet" }))).length,
asChute: structureTopLoops(flat).length,
};
}
process.stdout.write(JSON.stringify(out));