Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
/* PLAN 115 단위검증 헬퍼 — 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;
|
||||
// 화면 모듈(@ui · @config)은 코리도 계산이 부르지 않는다 — 빈 껍데기로 넘긴다.
|
||||
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 { deformTerrain } = loadTs(B05("B05_Profile_UI_Corridor_Deform_Terrain"));
|
||||
const { TerrainBandSplit } = loadTs(B05("B05_Profile_UI_Corridor_Split"));
|
||||
const { TerrainHeightIndex } = loadTs(B05("B05_Profile_UI_Corridor_Terrain"));
|
||||
|
||||
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:n). */
|
||||
function station(chainage, mode, groundZ, ratio) {
|
||||
const w = Math.abs(groundZ - ROAD_Z) * ratio;
|
||||
const pts = [
|
||||
[-30, groundZ],
|
||||
[-(HALF + w), groundZ],
|
||||
[-HALF, ROAD_Z],
|
||||
[-2.5, ROAD_Z],
|
||||
[2.5, ROAD_Z],
|
||||
[HALF, ROAD_Z],
|
||||
[HALF + w, groundZ],
|
||||
[30, groundZ],
|
||||
];
|
||||
const samples = [];
|
||||
for (let o = -30; o <= 30; o += 1) samples.push({ offset_m: o, elevation_m: groundZ });
|
||||
return {
|
||||
station_id: `S${chainage}`,
|
||||
chainage_m: chainage,
|
||||
label: "",
|
||||
kind: "regular",
|
||||
center_x: chainage,
|
||||
center_y: 0,
|
||||
center_z: groundZ,
|
||||
frame: { left_xy: [0, 1] },
|
||||
samples,
|
||||
design: {
|
||||
section_mode: mode,
|
||||
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 ROUTE = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 40, y: 0 },
|
||||
];
|
||||
|
||||
function fieldOf(sections, groundAt) {
|
||||
const build = buildCorridor(sections, ROUTE);
|
||||
const field = deform.buildDeformField(build, deform.stationProfileSource(sections), groundAt);
|
||||
return { build, field };
|
||||
}
|
||||
|
||||
const results = {};
|
||||
|
||||
// ① 성토 — 지반 96 · 1:1.5. 비탈 안 정점은 성토선으로 올라가고, 끝(6 m) 밖은 그대로.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const { build, field } = fieldOf(sections, () => 96);
|
||||
results.fill = {
|
||||
rows: field.rows.length,
|
||||
anchorL: field.rows[10].left.map(r4),
|
||||
left2: r4(field.deformZ(10, HALF + 2, 96)),
|
||||
right2: r4(field.deformZ(10, -HALF - 2, 96)),
|
||||
boundary: r4(field.deformZ(10, HALF, 96)),
|
||||
beyond: r4(field.deformZ(10, HALF + 9, 96)),
|
||||
// 지반이 성토선보다 높은 정점은 그대로(clamp 아래쪽만 올린다).
|
||||
above: r4(field.deformZ(10, HALF + 2, 99.5)),
|
||||
oldRibbons: build.ribbons.length,
|
||||
};
|
||||
}
|
||||
|
||||
// ② 첫 지반 교차점 밖은 원지반 — 비탈 끝(6 m) 너머 골짜기(80)는 연장선(92)으로 메우지 않는다.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const groundAt = (x, y) => (Math.abs(y) - HALF > 10 ? 80 : 96);
|
||||
const { field } = fieldOf(sections, groundAt);
|
||||
results.valley = {
|
||||
inside: r4(field.deformZ(10, HALF + 2, 96)),
|
||||
farValley: r4(field.deformZ(10, HALF + 15, 80)),
|
||||
};
|
||||
}
|
||||
|
||||
// ③ 절토 — 지반 104 · 1:1. 비탈 안 정점은 절토선으로 내려가고 끝(4 m) 밖은 그대로.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_cut", 104, 1.0));
|
||||
const { field } = fieldOf(sections, () => 104);
|
||||
results.cut = {
|
||||
left2: r4(field.deformZ(10, HALF + 2, 104)),
|
||||
beyond: r4(field.deformZ(10, HALF + 6, 104)),
|
||||
below: r4(field.deformZ(10, HALF + 2, 101)),
|
||||
};
|
||||
}
|
||||
|
||||
// ④ 측점 사이 보간 — 0 측점 1:1.5, 40 측점 1:2. 가운데(20)는 앵커 기준 상대 단면 보간.
|
||||
{
|
||||
const sections = [station(0, "both_fill", 96, 1.5), station(40, "both_fill", 96, 2.0)];
|
||||
const { field } = fieldOf(sections, () => 96);
|
||||
results.taper = {
|
||||
at0: r4(field.deformZ(0.1, HALF + 2, 96)),
|
||||
at20: r4(field.deformZ(20, HALF + 2, 96)),
|
||||
at40: r4(field.deformZ(39.9, HALF + 2, 96)),
|
||||
};
|
||||
}
|
||||
|
||||
// ⑤ 단면 밖 마지막 구배 연장 · 반대 측 빌리기 — 단면 함수 단독.
|
||||
{
|
||||
const profile = Float64Array.from([0, 0, 2, -1, 4, -2]);
|
||||
results.profile = {
|
||||
mid: r4(deform.profileZ(profile, 1)),
|
||||
extend: r4(deform.profileZ(profile, 10)),
|
||||
noExtend: deform.profileZ(profile, 10, true),
|
||||
};
|
||||
}
|
||||
|
||||
// ⑥ Three 지형 — 사본에만 · XY 고정 · 노면부 구멍 · 색 · 흑백 보존 표식.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const build = buildCorridor(sections, ROUTE);
|
||||
build.sections = sections;
|
||||
const bounds = { x: [-10, 50], y: [-20, 20], z: [90, 110] };
|
||||
const cx = 20;
|
||||
const cy = 0;
|
||||
const cz = 100;
|
||||
const step = 0.5;
|
||||
const nx = Math.round(60 / step) + 1;
|
||||
const ny = Math.round(40 / step) + 1;
|
||||
const positions = new Float32Array(nx * ny * 3);
|
||||
const colors = new Uint8Array(nx * ny * 4).fill(200);
|
||||
for (let j = 0; j < ny; j += 1) {
|
||||
for (let i = 0; i < nx; i += 1) {
|
||||
const v = j * nx + i;
|
||||
positions[v * 3] = -10 + i * step - cx;
|
||||
positions[v * 3 + 1] = 96 - cz;
|
||||
positions[v * 3 + 2] = -(-20 + j * step - cy);
|
||||
}
|
||||
}
|
||||
const index = [];
|
||||
for (let j = 0; j < ny - 1; j += 1) {
|
||||
for (let i = 0; i < nx - 1; i += 1) {
|
||||
const a = j * nx + i;
|
||||
index.push(a, a + 1, a + nx, a + 1, a + nx + 1, a + nx);
|
||||
}
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 4, true));
|
||||
geometry.setIndex(index);
|
||||
const terrain = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ vertexColors: true }));
|
||||
const before = positions.slice();
|
||||
const split = new TerrainBandSplit(terrain, { minX: -25, maxX: 25, minZ: -15, maxZ: 15 });
|
||||
const heightIndex = new TerrainHeightIndex(terrain);
|
||||
const out = deformTerrain(split, build, bounds, heightIndex);
|
||||
let xyMoved = 0;
|
||||
let inRoad = 0;
|
||||
let probe = null;
|
||||
let keepCount = 0;
|
||||
let tinted = 0;
|
||||
out.root.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh) || !child.userData.owned) return;
|
||||
const p = child.geometry.getAttribute("position");
|
||||
const c = child.geometry.getAttribute("color");
|
||||
for (let v = 0; v < nx * ny; v += 1) {
|
||||
if (p.getX(v) !== before[v * 3] || p.getZ(v) !== before[v * 3 + 2]) xyMoved += 1;
|
||||
const mx = p.getX(v) + cx;
|
||||
const my = -p.getZ(v) + cy;
|
||||
if (Math.abs(mx - 10) < 1e-6 && Math.abs(my - (HALF + 2)) < 1e-6) probe = p.getY(v) + cz;
|
||||
if (child.userData.keepColor[v]) {
|
||||
keepCount += 1;
|
||||
if (c.getX(v) !== 200 / 255) tinted += 1;
|
||||
}
|
||||
}
|
||||
const idx = child.geometry.getIndex();
|
||||
for (let t = 0; t < idx.count; t += 3) {
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
for (let k = 0; k < 3; k += 1) {
|
||||
sx += p.getX(idx.getX(t + k)) + cx;
|
||||
sy += -p.getZ(idx.getX(t + k)) + cy;
|
||||
}
|
||||
sx /= 3;
|
||||
sy /= 3;
|
||||
if (sx > 0.5 && sx < 39.5 && Math.abs(sy) < HALF - 0.05) inRoad += 1;
|
||||
}
|
||||
});
|
||||
results.three = {
|
||||
originalUntouched: before.every((value, i) => value === positions[i]),
|
||||
xyMoved,
|
||||
inRoad,
|
||||
probe: r4(probe),
|
||||
moved: out.stats.moved,
|
||||
raised: out.stats.raised,
|
||||
keepCount,
|
||||
tinted,
|
||||
};
|
||||
}
|
||||
|
||||
// ⑦ 115-2 상부 포락선 — 벽 앞면(수직)에서 안쪽으로 되돌아오는 계단은 위쪽만 남는다.
|
||||
{
|
||||
const { upperEnvelope } = loadTs(B05("B05_Profile_UI_Corridor_Deform_Structure"));
|
||||
const env = upperEnvelope(
|
||||
[
|
||||
[0, 0],
|
||||
[2, 0],
|
||||
[2.1, -3],
|
||||
[1.8, -3.2],
|
||||
[5, -4],
|
||||
].map(([offset_m, elevation_m]) => ({ offset_m, elevation_m })),
|
||||
);
|
||||
const at = (x) => env.find((p) => Math.abs(p.offset_m - x) < 1e-9)?.elevation_m;
|
||||
results.envelope = {
|
||||
monotone: env.every((p, i) => i === 0 || p.offset_m > env[i - 1].offset_m),
|
||||
at18: r4(at(1.8)),
|
||||
at21: r4(at(2.1)),
|
||||
at5: r4(at(5)),
|
||||
};
|
||||
}
|
||||
|
||||
/** 구조물 구간 단면(앵커 기준 상대 · 좌측) — 48 열로 다시 뽑는다. */
|
||||
function slopeOf(points, opts = {}) {
|
||||
const { resample } = loadTs(B05("B05_Profile_UI_Corridor_Station"));
|
||||
return {
|
||||
chainage_m: 20,
|
||||
side: "left",
|
||||
kind: "fill",
|
||||
span: { beforeM: 2, afterM: 2 },
|
||||
points: resample(
|
||||
points.map(([offset_m, elevation_m]) => ({ offset_m, elevation_m })),
|
||||
48,
|
||||
),
|
||||
endsAtStructure: false,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
// ⑧ 115-2 구조물 구간 고정 + 이웃 측점 테이퍼 — 20 측점 좌측에 선반 단면(4 m 수평 뒤 1:0.5).
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const build = buildCorridor(sections, ROUTE);
|
||||
const shelf = slopeOf([
|
||||
[0, 0],
|
||||
[4, 0],
|
||||
[6, -4],
|
||||
]);
|
||||
const source = deform.stationProfileSource(sections, [shelf]);
|
||||
const field = deform.buildDeformField(build, source, () => 96);
|
||||
results.span = {
|
||||
in20: r4(field.deformZ(20, HALF + 2, 96)),
|
||||
in19: r4(field.deformZ(19, HALF + 2, 96)),
|
||||
taper10: r4(field.deformZ(10, HALF + 2, 96)),
|
||||
taper30: r4(field.deformZ(30, HALF + 2, 96)),
|
||||
right20: r4(field.deformZ(20, -HALF - 2, 96)),
|
||||
};
|
||||
}
|
||||
|
||||
// ⑨ 115-2 BOX 「본체 밖 원지반」 — 구조물에서 끝나는 단면은 구간 안에서 끝 너머를 늘리지 않는다.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const build = buildCorridor(sections, ROUTE);
|
||||
const box = slopeOf(
|
||||
[
|
||||
[0, 0],
|
||||
[3, -1],
|
||||
],
|
||||
{ endsAtStructure: true },
|
||||
);
|
||||
const field = deform.buildDeformField(
|
||||
build,
|
||||
deform.stationProfileSource(sections, [box]),
|
||||
() => 96,
|
||||
);
|
||||
results.box = {
|
||||
inside: r4(field.deformZ(20, HALF + 2, 96)),
|
||||
outside: r4(field.deformZ(20, HALF + 5, 96)),
|
||||
normal: r4(field.deformZ(5, HALF + 5, 96)),
|
||||
};
|
||||
}
|
||||
|
||||
// ⑩ 115-2 날개 안쪽 성토 금지 — 고리 안은 원지반(천장 있으면 천장까지)만.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const build = buildCorridor(sections, ROUTE);
|
||||
const source = deform.stationProfileSource(sections);
|
||||
const loop = [
|
||||
[8, 4],
|
||||
[12, 4],
|
||||
[12, 8],
|
||||
[8, 8],
|
||||
];
|
||||
const none = deform.buildDeformField(build, source, () => 96, [{ loop, ceiling: null }]);
|
||||
const ceiling = deform.buildDeformField(build, source, () => 96, [{ loop, ceiling: 97 }]);
|
||||
const { noFillLimits } = loadTs(B05("B05_Profile_UI_Corridor_Deform_Structure"));
|
||||
const ring = (cx, cy) => ({ cx, cy, leftX: 0, leftY: 1, dz: 0 });
|
||||
const wings = [
|
||||
{ chainage_m: 20, kind: "revet", wing: "route", rings: [ring(18, 4), ring(16, 7)] },
|
||||
{ chainage_m: 20, kind: "revet", wing: "route", rings: [ring(22, 4), ring(24, 7)] },
|
||||
];
|
||||
const limits = noFillLimits(sections, wings);
|
||||
results.noFill = {
|
||||
blocked: r4(none.deformZ(10, HALF + 2, 96)),
|
||||
ceiling: r4(ceiling.deformZ(10, HALF + 2, 96)),
|
||||
outsideLoop: r4(none.deformZ(20, HALF + 2, 96)),
|
||||
limits: limits.length,
|
||||
hullArea: limits.length ? limits[0].loop.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ⑪ 115-2 구조물 풋프린트 구멍 — 20 측점 좌측 벽(편거리 5~6 · 19~21 m) 자리 삼각형 없음.
|
||||
{
|
||||
const sections = [0, 20, 40].map((c) => station(c, "both_fill", 96, 1.5));
|
||||
const build = buildCorridor(sections, ROUTE);
|
||||
build.sections = sections;
|
||||
const ring = (cx) => ({ cx, cy: 0, leftX: 0, leftY: 1, dz: 0 });
|
||||
build.structures = [
|
||||
{
|
||||
chainage_m: 20,
|
||||
kind: "revet",
|
||||
polygon: [
|
||||
[5, 95],
|
||||
[6, 95],
|
||||
[6, 100],
|
||||
[5, 100],
|
||||
],
|
||||
rings: [ring(19), ring(20), ring(21)],
|
||||
},
|
||||
];
|
||||
const step = 0.5;
|
||||
const cx = 20;
|
||||
const cz = 100;
|
||||
const positions = [];
|
||||
const nx = 121;
|
||||
const ny = 81;
|
||||
for (let j = 0; j < ny; j += 1) {
|
||||
for (let i = 0; i < nx; i += 1) positions.push(-10 + i * step - cx, 96 - cz, -(-20 + j * step));
|
||||
}
|
||||
const index = [];
|
||||
for (let j = 0; j < ny - 1; j += 1) {
|
||||
for (let i = 0; i < nx - 1; i += 1) {
|
||||
const a = j * nx + i;
|
||||
index.push(a, a + 1, a + nx, a + 1, a + nx + 1, a + nx);
|
||||
}
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setIndex(index);
|
||||
const terrain = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial());
|
||||
const split = new TerrainBandSplit(terrain, { minX: -25, maxX: 25, minZ: -15, maxZ: 15 });
|
||||
const out = deformTerrain(
|
||||
split,
|
||||
build,
|
||||
{ x: [-10, 50], y: [-20, 20], z: [90, 110] },
|
||||
new TerrainHeightIndex(terrain),
|
||||
);
|
||||
let inHole = 0;
|
||||
let nearHole = 0;
|
||||
out.root.traverse((child) => {
|
||||
if (!(child instanceof THREE.Mesh) || !child.userData.owned) return;
|
||||
const p = child.geometry.getAttribute("position");
|
||||
const idx = child.geometry.getIndex();
|
||||
for (let t = 0; t < idx.count; t += 3) {
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
for (let k = 0; k < 3; k += 1) {
|
||||
sx += p.getX(idx.getX(t + k)) + cx;
|
||||
sy += -p.getZ(idx.getX(t + k));
|
||||
}
|
||||
sx /= 3;
|
||||
sy /= 3;
|
||||
if (sx > 19.05 && sx < 20.95 && sy > 5.05 && sy < 5.95) inHole += 1;
|
||||
if (sx > 19.05 && sx < 20.95 && sy > 6.05 && sy < 6.6) nearHole += 1;
|
||||
}
|
||||
});
|
||||
results.hole = { holes: out.stats.holes, inHole, nearHole };
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(results));
|
||||
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 115 — B05 3D 새 방식(지형 메시 변형) 합성 격자 검사.
|
||||
|
||||
Node 도우미(helper_115_corridor_deform.cjs)가 TS 를 트랜스파일해 돌리고, 여기서는
|
||||
결과(JSON)를 단언한다. 정점 규칙 `z' = clamp(지형, 성토선, 절토선)` · 비탈 끝 밖 원지반 ·
|
||||
측점 사이 보간 · 단면 밖 마지막 구배 연장 · 사본에만 · XY 고정 · 노면부 구멍 · 색.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FILL_2M = round(100 - 2 / 1.5, 4)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def results():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_115_corridor_deform.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_fill_clamp_raises_to_fill_line(results):
|
||||
r = results["fill"]
|
||||
assert r["rows"] >= 2
|
||||
assert r["anchorL"][1:] == [3, 100]
|
||||
assert r["left2"] == FILL_2M
|
||||
assert r["right2"] == FILL_2M
|
||||
assert r["boundary"] == 100 # 노면부 끝 경계 정점 = 앵커 z
|
||||
assert r["beyond"] == 96 # 비탈 끝(6 m) 밖은 원지반
|
||||
assert r["above"] == 99.5 # 지반이 성토선 위면 그대로
|
||||
|
||||
|
||||
def test_fill_stops_at_first_catch(results):
|
||||
r = results["valley"]
|
||||
assert r["inside"] == FILL_2M
|
||||
assert r["farValley"] == 80 # 연장선(92)으로 골짜기를 메우지 않는다
|
||||
|
||||
|
||||
def test_cut_clamp_lowers_to_cut_line(results):
|
||||
r = results["cut"]
|
||||
assert r["left2"] == 102
|
||||
assert r["beyond"] == 104
|
||||
assert r["below"] == 101 # 지반이 절토선 아래면 그대로
|
||||
|
||||
|
||||
def test_station_interpolation(results):
|
||||
r = results["taper"]
|
||||
assert abs(r["at0"] - FILL_2M) < 0.02
|
||||
assert abs(r["at40"] - 99.0) < 0.02
|
||||
assert abs(r["at20"] - round(100 - 2 * 4 / 7, 4)) < 1e-3 # 상대 단면 보간
|
||||
|
||||
|
||||
def test_profile_extends_last_grade(results):
|
||||
r = results["profile"]
|
||||
assert r["mid"] == -0.5
|
||||
assert r["extend"] == -5
|
||||
assert r["noExtend"] is None
|
||||
|
||||
|
||||
def test_three_copy_only_xy_fixed_hole_and_tint(results):
|
||||
r = results["three"]
|
||||
assert r["originalUntouched"] is True # 원본 정점 버퍼 그대로([예상형상] OFF = 원본)
|
||||
assert r["xyMoved"] == 0
|
||||
assert r["inRoad"] == 0 # 노면부 안 삼각형 없음
|
||||
assert r["probe"] == FILL_2M
|
||||
assert r["moved"] > 0 and r["raised"] == r["moved"]
|
||||
assert r["keepCount"] > 0 and r["tinted"] == r["keepCount"]
|
||||
|
||||
|
||||
def test_upper_envelope_keeps_top_of_wall_steps(results):
|
||||
r = results["envelope"]
|
||||
assert r["monotone"] is True
|
||||
assert r["at18"] == 0 # 안쪽으로 되돌아온 계단(-3.2)보다 위쪽 마디(0)
|
||||
assert r["at21"] == -3
|
||||
assert r["at5"] == -4
|
||||
|
||||
|
||||
def test_structure_span_fixed_and_tapers_to_neighbours(results):
|
||||
r = results["span"]
|
||||
assert r["in20"] == 100 and r["in19"] == 100 # 구간(18~22) 안 = 선반 단면 고정
|
||||
assert FILL_2M < r["taper10"] < 100 # 구간 끝 → 이웃 측점 테이퍼
|
||||
assert r["taper30"] == r["taper10"]
|
||||
assert r["right20"] == FILL_2M # 반대 측은 측점 단면 그대로
|
||||
|
||||
|
||||
def test_box_outside_body_stays_original_ground(results):
|
||||
r = results["box"]
|
||||
assert r["inside"] == round(100 - 2 / 3, 4)
|
||||
assert r["outside"] == 96 # 단면 끝 너머 = 원지반(연장 안 함)
|
||||
assert 96 < r["normal"] < 100 # 구간 밖 테이퍼 행은 그대로 변형
|
||||
|
||||
|
||||
def test_no_fill_ring_inside_wings(results):
|
||||
r = results["noFill"]
|
||||
assert r["blocked"] == 96
|
||||
assert r["ceiling"] == 97
|
||||
assert r["outsideLoop"] == FILL_2M
|
||||
assert r["limits"] == 1 and r["hullArea"] == 4
|
||||
|
||||
|
||||
def test_structure_footprint_hole(results):
|
||||
r = results["hole"]
|
||||
assert r["holes"] == 1
|
||||
assert r["inHole"] == 0 # 풋프린트 안 지형 삼각형 없음
|
||||
assert r["nearHole"] > 0 # 바로 바깥은 남는다
|
||||
Reference in New Issue
Block a user