fix(B05): 3D 성토 비탈 끝 종단 이음 — 튐 · 못 닫힘 보간 · 측점 닻 원뿔 · 비탈 끝 밖 2 m 덧댐(115-7)
- 공통 변형 장(buildDeformField) 성토 비탈 끝 열을 측마다 종단으로 이음(새 ① · 제안 ② 같이 받음) - 못 닫힘 = 단면 끝 너머 max(2 m, 25 %) 넘어서야 닫힌 성토 · 끝까지 못 찾음 - 튐 = 이웃 ±3 m 중앙값과 max(2 m, 25 %) 넘게 다름 → 이웃 좋은 행 보간 - 원뿔 넓히기 1 m/m · 늘린 몫은 측점 값 + 1 m/m 까지 · 측점 행 날것 · 0 행 끊음(단면 있는 3 m 안 틈만 이음) - 새 ① 바뀐 행은 비탈 끝 밖 2 m 를 비탈 끝 높이 → 원지반으로 덧댐 · 절토 그대로 - 시험 test_115_7_fill_continuity(직각 골짜기 · 계단 지형 · 못 닫힘 · 측점 행 · 절토) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
* 보간 단면(제어점 · `_Frames.ts`) · 단면 밖은 마지막 구배 연장.
|
||||
* · 성토 · 절토는 앵커에서 이어진 자리(첫 지반 교차점 안)에만 — 비탈 끝 밖은 원지반.
|
||||
* 못 찾으면 단면 끝까지만(115-5 P0-1) · 높이 변화는 단면 높이 + 1 m 까지.
|
||||
* · 성토 비탈 끝은 종단으로 잇는다(115-7 smoothFillCatches) — 튐 · 못 닫힘 보간 · 원뿔 넓히기 ·
|
||||
* 바뀐 행은 비탈 끝 밖 2 m 덧댐.
|
||||
* · 조각은 앞뒤 측점 중 하나라도 가진 행에만(`_Build.ts` 와 같은 규칙).
|
||||
* · XY 는 움직이지 않는다.
|
||||
* ========================================================================== */
|
||||
@@ -50,6 +52,15 @@ const GRID_M = 4;
|
||||
const SEED_EVERY = 5;
|
||||
const WALK_LIMIT = 400;
|
||||
|
||||
// ponytail: 성토 비탈 끝 종단 이음(115-7) 고정 값 — 이웃 ±3 m 중앙값 · 튐 = max(2 m, 25 %) ·
|
||||
// 원뿔 1 m/m · 비탈 끝 밖 덧댐 2 m · 이을 짧은 틈 3 m. 지형마다 안 맞으면 여기서 조절.
|
||||
const FILL_WINDOW_M = 3;
|
||||
const FILL_JUMP_M = 2;
|
||||
const FILL_JUMP_RATIO = 0.25;
|
||||
const FILL_CONE = 1;
|
||||
const FILL_SKIRT_M = 2;
|
||||
const FILL_BRIDGE_M = 3;
|
||||
|
||||
const SIDES: readonly DeformSide[] = ["left", "right"];
|
||||
const KINDS: readonly DeformKind[] = ["fill", "cut"];
|
||||
|
||||
@@ -290,6 +301,105 @@ export function stationProfileSource(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 성토 비탈 끝 종단 이음(115-7) — 한 측 성토 열. 행마다 따로 찾은 비탈 끝이 이웃과 튀면(골짜기 ·
|
||||
* 계단 DTM · 못 닫힘) 두 행 혼합이 행 사이에 수직 벽(지느러미 · 빗살 구덩이)을 세운다.
|
||||
* 튐 · 못 닫힘 행 → 이웃 좋은 행 보간 → 원뿔 넓히기(이웃 차 1 m/m 안). 측점 행은 날것(못 닫힘 빼고).
|
||||
* 비탈 끝 0 행은 구간을 끊는다 — 단면은 있는데 앵커에서 막힌 3 m 안 틈만 잇는다.
|
||||
* `open` = 못 닫힘 · `station` = 측점 행 · `shaped` = 그 행에 성토 단면 있음(모두 0/1).
|
||||
*/
|
||||
export function smoothFillCatches(
|
||||
chainages: ArrayLike<number>,
|
||||
raw: ArrayLike<number>,
|
||||
open: ArrayLike<number>,
|
||||
station: ArrayLike<number>,
|
||||
shaped: ArrayLike<number>,
|
||||
): Float64Array {
|
||||
const n = raw.length;
|
||||
const out = Float64Array.from(raw);
|
||||
const on = new Uint8Array(n);
|
||||
const bridged = new Uint8Array(n);
|
||||
for (let r = 0; r < n; r += 1) on[r] = raw[r] > 0 ? 1 : 0;
|
||||
for (let r = 1; r < n; r += 1) {
|
||||
if (on[r] || !on[r - 1]) continue;
|
||||
let e = r;
|
||||
while (e < n && !on[e] && shaped[e]) e += 1;
|
||||
if (e < n && on[e] && chainages[e - 1] - chainages[r] <= FILL_BRIDGE_M) {
|
||||
for (let k = r; k < e; k += 1) on[k] = bridged[k] = 1;
|
||||
}
|
||||
r = e;
|
||||
}
|
||||
for (let start = 0; start < n;) {
|
||||
if (!on[start]) {
|
||||
start += 1;
|
||||
continue;
|
||||
}
|
||||
let end = start;
|
||||
while (end + 1 < n && on[end + 1]) end += 1;
|
||||
const trusted = (k: number) => !open[k] && !bridged[k];
|
||||
const good = new Uint8Array(end - start + 1);
|
||||
let lo = start;
|
||||
let hi = start;
|
||||
for (let r = start; r <= end; r += 1) {
|
||||
if (station[r] && trusted(r)) {
|
||||
good[r - start] = 1;
|
||||
continue;
|
||||
}
|
||||
while (chainages[lo] < chainages[r] - FILL_WINDOW_M) lo += 1;
|
||||
while (hi < end && chainages[hi + 1] <= chainages[r] + FILL_WINDOW_M) hi += 1;
|
||||
const near: number[] = [];
|
||||
for (let k = lo; k <= hi; k += 1) if (trusted(k)) near.push(raw[k]);
|
||||
if (!near.length) {
|
||||
good[r - start] = trusted(r) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
near.sort((a, b) => a - b);
|
||||
const median = near[near.length >> 1];
|
||||
const jump = Math.max(FILL_JUMP_M, median * FILL_JUMP_RATIO);
|
||||
good[r - start] = trusted(r) && Math.abs(raw[r] - median) <= jump ? 1 : 0;
|
||||
}
|
||||
// 튐 · 못 닫힘 — 앞뒤 좋은 행을 노선 길이로 보간(한쪽뿐이면 그 값 · 없으면 날것).
|
||||
let prev = -1;
|
||||
for (let r = start; r <= end; r += 1) {
|
||||
if (good[r - start]) {
|
||||
prev = r;
|
||||
continue;
|
||||
}
|
||||
let next = r + 1;
|
||||
while (next <= end && !good[next - start]) next += 1;
|
||||
if (prev >= 0 && next <= end) {
|
||||
const t = (chainages[r] - chainages[prev]) / (chainages[next] - chainages[prev] || 1);
|
||||
out[r] = raw[prev] + (raw[next] - raw[prev]) * t;
|
||||
} else if (prev >= 0) out[r] = raw[prev];
|
||||
else if (next <= end) out[r] = raw[next];
|
||||
}
|
||||
// 원뿔 넓히기 — c'_r = max_j(c_j − 1·|s_r − s_j|) 를 앞뒤 두 번 훑어 낸다. 늘린 몫은 측점 닻
|
||||
// (측점 값 + 1 m/m)까지만 — 먼 큰 값이 측점까지 번지지 않게. 보간 값보다 줄이지는 않는다.
|
||||
const base = out.slice(start, end + 1);
|
||||
const cap = new Float64Array(end - start + 1).fill(Infinity);
|
||||
for (let r = start + 1; r <= end; r += 1) {
|
||||
out[r] = Math.max(out[r], out[r - 1] - FILL_CONE * (chainages[r] - chainages[r - 1]));
|
||||
}
|
||||
for (let r = end - 1; r >= start; r -= 1) {
|
||||
out[r] = Math.max(out[r], out[r + 1] - FILL_CONE * (chainages[r + 1] - chainages[r]));
|
||||
}
|
||||
for (const step of [1, -1]) {
|
||||
let anchor = -1;
|
||||
for (let r = step > 0 ? start : end; r >= start && r <= end; r += step) {
|
||||
if (station[r] && trusted(r)) anchor = r;
|
||||
if (anchor < 0) continue;
|
||||
const reach = raw[anchor] + FILL_CONE * Math.abs(chainages[r] - chainages[anchor]);
|
||||
cap[r - start] = Math.min(cap[r - start], reach);
|
||||
}
|
||||
}
|
||||
for (let r = start; r <= end; r += 1) {
|
||||
out[r] = Math.max(base[r - start], Math.min(out[r], cap[r - start]));
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 점이 고리 안인가(광선 투사). */
|
||||
function insideLoop(loop: ReadonlyArray<[number, number]>, x: number, y: number): boolean {
|
||||
let hit = false;
|
||||
@@ -325,6 +435,8 @@ export function buildDeformField(
|
||||
const heights = new Float64Array(n * 4);
|
||||
const bends: Array<Array<[number, number]>> = new Array(n * 4);
|
||||
const noExtend = new Uint8Array(n * 2);
|
||||
/** 비탈 끝을 단면 끝까지 못 찾음(못 닫힘). */
|
||||
const open = new Uint8Array(n * 4);
|
||||
let reachMax = 0;
|
||||
rows.forEach((row, r) => {
|
||||
const dx = row.left[0] - row.right[0];
|
||||
@@ -379,22 +491,62 @@ export function buildDeformField(
|
||||
previous = now;
|
||||
}
|
||||
// 못 찾으면 단면 끝까지만 — 마지막 구배로 60 m 까지 깎던 폭주 구덩이(115-5 P0-1 ①).
|
||||
if (!found) reach = profile[(count - 1) * 2];
|
||||
const end = profile[(count - 1) * 2];
|
||||
if (!found) reach = end;
|
||||
// 못 닫힘(115-7) — 단면 끝(측점 = B06 비탈 끝) 너머 튐 폭보다 멀리서야 닫힌 성토. 연장 구배를
|
||||
// 안전 상한이 눌러 원지반 위 띠(지느러미)가 되던 행 — 이웃 좋은 행으로 보간한다.
|
||||
const slack = Math.max(FILL_JUMP_M, end * FILL_JUMP_RATIO);
|
||||
if (!found || (kind === "fill" && reach > end + slack)) open[slot] = 1;
|
||||
}
|
||||
catches[slot] = reach;
|
||||
if (reach > reachMax) reachMax = reach;
|
||||
// 단면 꺾임(암/토사 · 소단) — 비탈 끝 안쪽만.
|
||||
for (let i = 1; i < count - 1; i += 1) {
|
||||
const s = profile[i * 2];
|
||||
if (s >= reach) break;
|
||||
const a0 = Math.atan2(profile[i * 2 + 1] - profile[i * 2 - 1], s - profile[i * 2 - 2]);
|
||||
const a1 = Math.atan2(profile[i * 2 + 3] - profile[i * 2 + 1], profile[i * 2 + 2] - s);
|
||||
if (Math.abs(a1 - a0) * (180 / Math.PI) > BEND_DEG) bends[slot].push([i, s]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 성토 비탈 끝 종단 이음(115-7) — 짧아진 행 · 못 닫힘 행은 비탈 끝 밖을 덧댄다.
|
||||
const chainages = rows.map((row) => row.chainage_m);
|
||||
const station = new Uint8Array(n);
|
||||
for (const section of build.sections ?? []) {
|
||||
let r = 0;
|
||||
while (r + 1 < n && chainages[r + 1] <= section.chainage_m) r += 1;
|
||||
if (r + 1 < n && chainages[r + 1] - section.chainage_m < section.chainage_m - chainages[r]) {
|
||||
r += 1;
|
||||
}
|
||||
if (Math.abs(chainages[r] - section.chainage_m) < 0.15) station[r] = 1;
|
||||
}
|
||||
const skirt = new Uint8Array(n * 4);
|
||||
for (let si = 0; si < 2; si += 1) {
|
||||
const pick = <T>(of: (slot: number) => T) => rows.map((_, r) => of(r * 4 + si * 2));
|
||||
const raw = pick((slot) => catches[slot]);
|
||||
const final = smoothFillCatches(
|
||||
chainages,
|
||||
raw,
|
||||
pick((slot) => open[slot]),
|
||||
station,
|
||||
pick((slot) => (profiles[slot] ? 1 : 0)),
|
||||
);
|
||||
final.forEach((c, r) => {
|
||||
const slot = r * 4 + si * 2;
|
||||
catches[slot] = c;
|
||||
if (c > 0 && (open[slot] || c < raw[r] - 0.01 || raw[r] <= 0)) skirt[slot] = 1;
|
||||
});
|
||||
}
|
||||
for (let slot = 0; slot < n * 4; slot += 1) {
|
||||
const reach = catches[slot];
|
||||
reachMax = Math.max(reachMax, reach + (skirt[slot] ? FILL_SKIRT_M : 0));
|
||||
const profile = profiles[slot];
|
||||
if (!profile) continue;
|
||||
const count = profile.length / 2;
|
||||
// 단면 꺾임(암/토사 · 소단) — 비탈 끝 안쪽만.
|
||||
for (let i = 1; i < count - 1; i += 1) {
|
||||
const s = profile[i * 2];
|
||||
if (s >= reach) break;
|
||||
const a0 = Math.atan2(profile[i * 2 + 1] - profile[i * 2 - 1], s - profile[i * 2 - 2]);
|
||||
const a1 = Math.atan2(profile[i * 2 + 3] - profile[i * 2 + 1], profile[i * 2 + 2] - s);
|
||||
if (Math.abs(a1 - a0) * (180 / Math.PI) > BEND_DEG) bends[slot].push([i, s]);
|
||||
}
|
||||
}
|
||||
|
||||
// 행 중심 격자 — 칸마다 가장 가까운 씨앗 행. 정점은 그 행에서 이웃으로 걸어 자리를 잡는다.
|
||||
let maxHalf = 0;
|
||||
hw.forEach((value) => (maxHalf = Math.max(maxHalf, value)));
|
||||
@@ -482,10 +634,15 @@ export function buildDeformField(
|
||||
KINDS.forEach((kind, ki) => {
|
||||
const slot = r * 4 + si * 2 + ki;
|
||||
const profile = profiles[slot];
|
||||
if (!profile || s >= catches[slot]) return;
|
||||
const rel = profileZ(profile, s, free);
|
||||
const reach = catches[slot];
|
||||
if (!profile || reach <= 0) return;
|
||||
// 덧댐(115-7) — 이은 비탈 끝 밖 2 m 는 비탈 끝 높이에서 원지반으로 내려 수직 면을 없앤다.
|
||||
if (s >= reach && (!skirt[slot] || s >= reach + FILL_SKIRT_M)) return;
|
||||
const pad = s < reach ? 0 : (s - reach) / FILL_SKIRT_M;
|
||||
const rel = profileZ(profile, Math.min(s, reach), free);
|
||||
if (rel === null) return;
|
||||
let line = anchor[2] + rel;
|
||||
line += (z - line) * pad;
|
||||
// 안전 상한(115-5 P0-1 ④) — 단면 높이 + 1 m 넘게는 옮기지 않는다. 구조물에서 끝나는
|
||||
// 단면(BOX)은 지반까지 안 내려가 단면 높이가 기준이 못 된다.
|
||||
const cap = heights[slot] + SAFETY_M;
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/* 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));
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 115-7 — B05 3D 성토 비탈 끝 종단 이음(공통 변형 장) 합성 검사.
|
||||
|
||||
Node 도우미(helper_115_7_fill_continuity.cjs)가 TS 를 트랜스파일해 돌리고, 여기서는
|
||||
결과(JSON)를 단언한다. 직각 골짜기 지느러미 0 · 덧댐 · 계단 지형 출렁임 · 못 닫힘 보간 ·
|
||||
측점 행 날것 · 0 행 끊음 · 절토 그대로.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def results():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_115_7_fill_continuity.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_valley_has_no_fins(results):
|
||||
r = results["valley"]
|
||||
assert r["fins"] == 0
|
||||
assert (
|
||||
r["gullyMax"] == 6 and r["gullyMin"] == 6
|
||||
) # 골 행도 이웃 비탈 끝(6 m) — 10.5 m 로 안 뻗음
|
||||
|
||||
|
||||
def test_valley_skirt_replaces_wall(results):
|
||||
r = results["valley"]
|
||||
assert r["skirtMid"] == 94.5 # 비탈 끝 밖 1 m — 96 → 93 반쯤
|
||||
assert r["skirtOut"] == 93 # 덧댐 밖 — 원지반
|
||||
assert r["outside"] == 96 # 골 밖 — 원지반 그대로
|
||||
|
||||
|
||||
def test_cut_unchanged(results):
|
||||
r = results["valley"]
|
||||
assert r["cutFlat"] == 4
|
||||
assert 2 < r["cutGully"] < 2.3 # 절토는 골을 날것 그대로 따른다
|
||||
|
||||
|
||||
def test_step_terrain_wobble_small(results):
|
||||
r = results["steps"]
|
||||
assert r["fins"] == 0
|
||||
assert r["wobble"] < 0.7 * r["rawWobble"]
|
||||
|
||||
|
||||
def test_open_rows_interpolated(results):
|
||||
r = results["column"]
|
||||
assert r["openMax"] == 6 # 못 닫힘(15) → 이웃 6
|
||||
assert r["stationOpen"] < 9 # 못 닫힘 측점도 보간(날것 20 아님)
|
||||
|
||||
|
||||
def test_station_row_kept_and_zero_breaks(results):
|
||||
r = results["column"]
|
||||
assert r["stationKept"] == 9 # 측점 행 날것
|
||||
assert r["bridged"] == 6 # 단면 있는 짧은 틈 — 잇는다
|
||||
assert r["cutGap"] == 0 # 단면 없는 행 — 0 그대로(구간 끊음)
|
||||
assert r["neighbourStep"] <= 1 + 1e-6 # 이웃 행 차 1 m/m 안
|
||||
Reference in New Issue
Block a user