- 단면 = B06 횡단 띠와 한 벌(Wall_Hatch chuteBandOutline 로 떼어 냄 · appendChuteBand 가 같이 씀) - 관 = 유출 벽 길이 · BOX = 구체 폭(축 고정) · 세월교 = 월류 폭 · 물넘이 포장(ford_chute) = 포장 폭 · 주인 측점 하나 - 구조물 종류 chute · 색 · 끝 마구리 = 마디마다 사다리꼴 · 해시 = 고른 때만 ch 키(안 고른 측점 저장본 그대로) - ③ structureTopLoops 가 chute 를 뺌 - 새 시험 test_135_3_chute_solid(+ 도우미) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
271 lines
10 KiB
JavaScript
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("B05_Profile_UI_Corridor_Structures_Hash"));
|
|
const { structureTopLoops } = loadTs(B05("B05_Profile_UI_Corridor_Tin_Design"));
|
|
const { chuteBandOutline } = 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((g) => g.kind !== "cut")
|
|
.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));
|