- 뷰어 = loftTerrain 바로 부름 · ③ 못 만들면 원지형 그대로(_Clip 도 Old) - 통째 이동 = _Modes · _Legacy · _Deform_Terrain · _Deform_Breakline · _Clip - 잘라 냄(원본은 Old 같은 이름) = 뷰어코리도 · 보기줄 · _Corridor · _Build legacy · _Station silhouette · _Deform(roadRows 만) · _Deform_Structure(noFillLimits 만) · _Tin(deformDesign) · _Tin_Design(buildTinDesign) · _Region(CellRegion 까지) - b_page_state 키 셋(corridor-deform · legacy · mode) 정리 - 시험 6개 + 도우미 5 = resources/tester/old/(수집 뺌) · test_115_8 등록부 단언 = ③ 고정 단언 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
211 lines
7.6 KiB
JavaScript
211 lines
7.6 KiB
JavaScript
/* PLAN 115-7 단위검증 헬퍼 — B05 3D 성토 비탈 끝 종단 이음(공통 변형 장 · 새 ① · 제안 ②).
|
||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌린다. 결과는 표준출력에 JSON 한 벌.
|
||
*
|
||
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다.
|
||
*/
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const ROOT = path.join(__dirname, "..", "..");
|
||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||
const THREE = require(path.join(ROOT, "config", "node_modules", "three"));
|
||
|
||
const cache = new Map();
|
||
function loadTs(file) {
|
||
const full = path.resolve(file);
|
||
if (cache.has(full)) return cache.get(full);
|
||
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 localRequire = (request) => {
|
||
if (request === "three") return THREE;
|
||
if (request.startsWith("@ui/") || request.startsWith("@config/")) {
|
||
return new Proxy({}, { get: () => () => undefined });
|
||
}
|
||
const target = request.startsWith("@util/")
|
||
? path.join(ROOT, "common_util", request.slice(6))
|
||
: path.join(path.dirname(full), request);
|
||
if (request.endsWith(".json")) return JSON.parse(fs.readFileSync(target, "utf8"));
|
||
return loadTs(fs.existsSync(`${target}.ts`) ? `${target}.ts` : path.join(target, "index.ts"));
|
||
};
|
||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||
return box.exports;
|
||
}
|
||
|
||
const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`);
|
||
const { buildCorridor } = loadTs(B05("B05_Profile_UI_Corridor_Build"));
|
||
const deform = loadTs(B05("B05_Profile_UI_Corridor_Deform"));
|
||
|
||
const r4 = (v) => (v === null || v === undefined ? v : Math.round(v * 10000) / 10000);
|
||
const HALF = 3; // 노면부 반폭(차도 2.5 + 노견 0.5)
|
||
const ROAD_Z = 100;
|
||
|
||
/** x 축 직선 노선 측점 — 좌 성토(1:1.5 · 4 m 아래 · 비탈 끝 9) · 우 절토(1:1 · 4 m 위 · 비탈 끝 −7). */
|
||
function station(chainage) {
|
||
const pts = [
|
||
[-7, 104],
|
||
[-HALF, ROAD_Z],
|
||
[-2.5, ROAD_Z],
|
||
[2.5, ROAD_Z],
|
||
[HALF, ROAD_Z],
|
||
[9, 96],
|
||
];
|
||
const samples = [];
|
||
for (let o = -30; o <= 30; o += 1) samples.push({ offset_m: o, elevation_m: o > 0 ? 96 : 104 });
|
||
return {
|
||
station_id: `S${chainage}`,
|
||
chainage_m: chainage,
|
||
label: "",
|
||
kind: "regular",
|
||
center_x: chainage,
|
||
center_y: 0,
|
||
center_z: ROAD_Z,
|
||
frame: { left_xy: [0, 1] },
|
||
samples,
|
||
design: {
|
||
section_mode: "right_cut",
|
||
road_edges: {
|
||
left: { offset_m: HALF, elevation_m: ROAD_Z },
|
||
right: { offset_m: -HALF, elevation_m: ROAD_Z },
|
||
},
|
||
carriageway_edges: {
|
||
left: { offset_m: 2.5, elevation_m: ROAD_Z },
|
||
right: { offset_m: -2.5, elevation_m: ROAD_Z },
|
||
},
|
||
design_line: pts.map(([offset_m, elevation_m]) => ({ offset_m, elevation_m })),
|
||
ditch: { type: "none" },
|
||
ditch_enabled: false,
|
||
ditch_side: "left",
|
||
},
|
||
};
|
||
}
|
||
|
||
const SECTIONS = [0, 20, 40, 60, 80, 100].map(station);
|
||
|
||
function fieldOf(groundAt) {
|
||
const build = buildCorridor(SECTIONS, [
|
||
{ x: 0, y: 0 },
|
||
{ x: 100, y: 0 },
|
||
]);
|
||
build.sections = SECTIONS;
|
||
const field = deform.buildDeformField(build, deform.stationProfileSource(SECTIONS), groundAt);
|
||
return field;
|
||
}
|
||
|
||
/** 행 · 측 성토 비탈 끝 열(노선 길이 순). */
|
||
function catches(field, si, ki) {
|
||
return field.rows.map((row, r) => ({ ch: row.chainage_m, c: field.catchAt(r, si, ki) }));
|
||
}
|
||
|
||
/** 지느러미 행 — 성토 행끼리 ±3 m 중앙값 + 3 m 넘음. */
|
||
function fins(list) {
|
||
let count = 0;
|
||
for (const { ch, c } of list) {
|
||
if (c <= 0) continue;
|
||
const near = list
|
||
.filter((o) => o.c > 0 && Math.abs(o.ch - ch) <= 3)
|
||
.map((o) => o.c)
|
||
.sort((a, b) => a - b);
|
||
if (c > near[near.length >> 1] + 3) count += 1;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
const wobble = (list) => {
|
||
let sum = 0;
|
||
for (let i = 1; i < list.length; i += 1) {
|
||
if (list[i].c > 0 && list[i - 1].c > 0) sum += Math.abs(list[i].c - list[i - 1].c);
|
||
}
|
||
return (sum / (list[list.length - 1].ch - list[0].ch)) * 100;
|
||
};
|
||
|
||
const results = {};
|
||
|
||
// ① 도로에 직각인 골짜기 — 좁은 골(49~51 m) · 넓은 골(64~72 m)에서 지반이 3 m 더 깊다. 옛 규칙은
|
||
// 골 행만 연장 구배로 10.5 m 까지 뻗어 지느러미 · 수직 벽. 새 규칙은 이웃 좋은 행(6 m)으로 보간 +
|
||
// 비탈 끝 밖 2 m 덧댐. 우측 절토는 같은 골을 그대로 따른다(절토는 안 바꿈).
|
||
{
|
||
const gully = (x) => (x > 49 && x < 51) || (x > 64 && x < 72);
|
||
const groundAt = (x, y) => {
|
||
if (y >= 0) return gully(x) && y > 5 ? 93 : 96;
|
||
return gully(x) && y < -5 ? 101 : 104;
|
||
};
|
||
const field = fieldOf(groundAt);
|
||
const fill = catches(field, 0, 0);
|
||
const inGully = fill.filter((o) => gully(o.ch));
|
||
const cutGully = catches(field, 1, 1).filter((o) => gully(o.ch) && o.ch > 50 && o.ch < 50.5);
|
||
const zAt = (x, s) => field.deformZ(x, HALF + s, groundAt(x, HALF + s));
|
||
results.valley = {
|
||
fins: fins(fill),
|
||
gullyMax: r4(Math.max(...inGully.map((o) => o.c))),
|
||
gullyMin: r4(Math.min(...inGully.map((o) => o.c))),
|
||
skirtMid: r4(zAt(68, 7)), // 비탈 끝(6) 밖 1 m — 96 에서 93 으로 반쯤
|
||
skirtOut: r4(zAt(68, 8.5)), // 덧댐 밖 — 원지반 그대로
|
||
outside: r4(zAt(30, 8)), // 골 밖 비탈 끝 밖 — 원지반
|
||
cutGully: r4(Math.max(...cutGully.map((o) => o.c))),
|
||
cutFlat: r4(
|
||
field.catchAt(
|
||
field.rows.findIndex((row) => row.chainage_m >= 30),
|
||
1,
|
||
1,
|
||
),
|
||
),
|
||
};
|
||
}
|
||
|
||
// ② 계단 지형 — 1 m 띠마다 지반이 0.6 m 오르내린다(비탈 끝 6 ↔ 5.1). 원뿔 넓히기로 출렁임이 준다.
|
||
{
|
||
const step = (x) => (Math.floor(x) % 2 ? 0.6 : 0);
|
||
const groundAt = (x, y) => (y >= 0 ? 96 + step(x) : 104);
|
||
const field = fieldOf(groundAt);
|
||
const fill = catches(field, 0, 0).filter((o) => o.ch > 1 && o.ch < 99);
|
||
// 날것 출렁임 — 띠 경계마다 0.9 m(6 − 5.1) 씩.
|
||
const raw = fill.map((o) => ({ ch: o.ch, c: step(o.ch) ? 5.1 : 6 }));
|
||
results.steps = { wobble: r4(wobble(fill)), rawWobble: r4(wobble(raw)), fins: fins(fill) };
|
||
}
|
||
|
||
// ③ ④ ⑤ 열 단위 — 못 닫힘 보간 · 측점 행 날것 · 0 행 끊음 / 짧은 틈 이음.
|
||
{
|
||
const n = 101;
|
||
const ch = Array.from({ length: n }, (_, i) => i * 0.2);
|
||
const raw = new Array(n).fill(6);
|
||
const open = new Array(n).fill(0);
|
||
const station = new Array(n).fill(0);
|
||
const shaped = new Array(n).fill(1);
|
||
for (let i = 20; i <= 24; i += 1) {
|
||
raw[i] = 15;
|
||
open[i] = 1;
|
||
}
|
||
station[50] = 1;
|
||
raw[50] = 9; // 측점 행 — 튀어도 날것
|
||
station[60] = 1;
|
||
raw[60] = 20;
|
||
open[60] = 1; // 못 닫힘 측점 — 보간
|
||
for (let i = 70; i <= 72; i += 1) raw[i] = 0; // 짧은 틈(0.4 m) — 단면 있음 → 잇는다
|
||
for (let i = 80; i <= 82; i += 1) {
|
||
raw[i] = 0;
|
||
shaped[i] = 0; // 단면 없음 → 끊음
|
||
}
|
||
const out = deform.smoothFillCatches(ch, raw, open, station, shaped);
|
||
const gap = [];
|
||
for (let i = 0; i < n; i += 1) if (raw[i] === 0 && shaped[i] === 0) gap.push(out[i]);
|
||
results.column = {
|
||
openMax: r4(Math.max(...out.slice(20, 25))),
|
||
stationKept: r4(out[50]),
|
||
stationOpen: r4(out[60]),
|
||
bridged: r4(Math.min(...out.slice(70, 73))),
|
||
cutGap: r4(Math.max(...gap)),
|
||
neighbourStep: r4(
|
||
Math.max(
|
||
...out
|
||
.slice(1)
|
||
.map((c, i) => (out[i] > 0 && c > 0 ? Math.abs(c - out[i]) / 0.2 : 0))
|
||
.filter((_, i) => i < 49 || i > 51),
|
||
),
|
||
),
|
||
};
|
||
}
|
||
|
||
process.stdout.write(JSON.stringify(results));
|