Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -52,6 +52,8 @@ const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`);
|
||||
const { buildCorridor } = loadTs(B05("B05_Profile_UI_Corridor_Build"));
|
||||
const { loftTerrain } = loadTs(B05("B05_Profile_UI_Corridor_Loft"));
|
||||
const { pairProfiles } = loadTs(B05("B05_Profile_UI_Corridor_Loft_Profile"));
|
||||
const { selfCrossings } = loadTs(B05("B05_Profile_UI_Corridor_Loft_Toe"));
|
||||
const { taubin } = loadTs(B05("B05_Profile_UI_Corridor_Loft_Grid"));
|
||||
const { TerrainBandSplit } = loadTs(B05("B05_Profile_UI_Corridor_Split"));
|
||||
const { TerrainHeightIndex } = loadTs(B05("B05_Profile_UI_Corridor_Terrain"));
|
||||
const PSLG = loadTs(B05("B05_Profile_UI_Corridor_Tin_Pslg"));
|
||||
@@ -158,6 +160,10 @@ function trianglesOf(root) {
|
||||
const v = idx ? idx.getX(t + k) : t + k;
|
||||
tri.push([p.getX(v), p.getY(v), p.getZ(v)]);
|
||||
}
|
||||
// 평면 넓이 0(한 줄에 선 세 점) 삼각형은 보이지 않는다 — 틈 셈에서 뺀다(115-10).
|
||||
const [a, b, c] = tri;
|
||||
const area = (b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2]);
|
||||
if (Math.abs(area) < 1e-9) continue;
|
||||
out.push({ tri, tin: child.name === "terrain-tin-mesh" });
|
||||
}
|
||||
});
|
||||
@@ -277,6 +283,42 @@ function lostVertices(tris, before, out) {
|
||||
|
||||
const results = {};
|
||||
|
||||
/** 사면(구역 색) 삼각형 가로세로비(가장 긴 변² / 2 · 넓이) — 10 넘는 수 · 가장 큰 값(115-10). */
|
||||
function aspectOf(out) {
|
||||
const mesh = out.root.getObjectByName("terrain-tin-mesh");
|
||||
const p = mesh.geometry.getAttribute("position");
|
||||
const keep = mesh.userData.keepColor;
|
||||
let over = 0;
|
||||
let max = 0;
|
||||
let count = 0;
|
||||
for (let t = 0; t < p.count / 3; t += 1) {
|
||||
if (!keep[t * 3]) continue;
|
||||
const v = [0, 1, 2].map((k) => new THREE.Vector3().fromBufferAttribute(p, t * 3 + k));
|
||||
const l = [0, 1, 2].map((k) => v[k].distanceTo(v[(k + 1) % 3]));
|
||||
const area = v[1].clone().sub(v[0]).cross(v[2].clone().sub(v[0])).length() / 2;
|
||||
const ratio = area > 1e-12 ? Math.max(...l) ** 2 / (2 * area) : Infinity;
|
||||
count += 1;
|
||||
if (ratio > 10) over += 1;
|
||||
max = Math.max(max, ratio);
|
||||
}
|
||||
return { count, over, max: Math.round(max * 100) / 100 };
|
||||
}
|
||||
|
||||
/** 설계 구역 둘레에서 한 측 비탈 끝 선(노면 끝 뺀 차례) 스스로 꼬임 수. */
|
||||
function toeCrossings(out) {
|
||||
const pslg = out.design.pslg;
|
||||
const tag = new Map();
|
||||
for (let i = 0; i < pslg.count; i += 1) tag.set(`${pslg.x[i]},${pslg.z[i]}`, pslg.tag[i]);
|
||||
const xs = [];
|
||||
const zs = [];
|
||||
out.design.outer.forEach(([x, z]) => {
|
||||
if (tag.get(`${x},${z}`) === PSLG.TAG_ANCHOR) return;
|
||||
xs.push(x);
|
||||
zs.push(z);
|
||||
});
|
||||
return selfCrossings(xs, zs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사면 음영(115-9) — 이웃한 사면(구역 색) 삼각형 짝마다 면 꺾임 각 · 같은 정점의 법선 차(음영 단차).
|
||||
* 꺾임 각 30° 안 짝은 법선이 같아야(부드러움) · 넘는 짝(소단)은 법선이 갈려야(또렷).
|
||||
@@ -418,6 +460,7 @@ function daylightPoints(out, bounds) {
|
||||
const wAt = (x) => widthAt.get(Math.round(x * 100));
|
||||
const kink = Math.abs((wAt(20) - wAt(19)) - (wAt(21) - wAt(20)));
|
||||
results.straight = {
|
||||
aspect: aspectOf(out),
|
||||
stats: out.stats,
|
||||
gaps: gap.gaps,
|
||||
overshoot: r6(overshoot),
|
||||
@@ -448,12 +491,17 @@ function daylightPoints(out, bounds) {
|
||||
const pts = daylightPoints(out, bounds);
|
||||
let wrongSide = 0;
|
||||
let offsetErr = 0;
|
||||
let overshoot = 0;
|
||||
pts.forEach((p) => {
|
||||
if (p.x < 0 || p.x > 20) return;
|
||||
if (p.tag === PSLG.TAG_FILL && p.x >= 10 + 1e-6) wrongSide += 1;
|
||||
if (p.tag === PSLG.TAG_CUT && p.x < 10 - 0.2 - 1e-6) wrongSide += 1;
|
||||
const w = 6 + ((4 - 6) * p.x) / 20;
|
||||
offsetErr = Math.max(offsetErr, Math.abs(Math.abs(p.y) - HALF - w));
|
||||
const w = Math.abs(p.y) - HALF;
|
||||
// 측점(0 · 20)에서 B06 오프셋 6 · 4 · 사이는 두 값 안(115-10 비탈 끝 고르기).
|
||||
if (Math.abs(p.x) < 1e-6 || Math.abs(p.x - 20) < 1e-6) {
|
||||
offsetErr = Math.max(offsetErr, Math.abs(w - (p.x < 10 ? 6 : 4)));
|
||||
}
|
||||
overshoot = Math.max(overshoot, w - 6, 4 - w);
|
||||
});
|
||||
// 성토 점 ↔ 절토 점을 잇는 제약 변(노면 끝 · 지형 말고) — 끊었으면 횡방향으로도 없어야 한다.
|
||||
const pslg = out.design.pslg;
|
||||
@@ -477,6 +525,7 @@ function daylightPoints(out, bounds) {
|
||||
cuts,
|
||||
crossEdges,
|
||||
offsetErr: r6(offsetErr),
|
||||
overshoot: r6(overshoot),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -532,9 +581,59 @@ function daylightPoints(out, bounds) {
|
||||
}
|
||||
}
|
||||
const gap = gapEdges(trianglesOf(out.root), out, frameOf(bounds, -40, 40, -30, 50));
|
||||
results.curve = { stats: out.stats, selfCross, gaps: gap.gaps };
|
||||
results.curve = {
|
||||
stats: out.stats,
|
||||
selfCross,
|
||||
toeCross: toeCrossings(out),
|
||||
aspect: aspectOf(out),
|
||||
gaps: gap.gaps,
|
||||
};
|
||||
}
|
||||
|
||||
// ④-2 직선 20 m → 좌 곡선(90°) → 직선 20 m · 높은 성토(8 m · 비탈 12 m) — 곡선 안쪽 두 직선의 비탈 끝이
|
||||
// 엇갈리는 자리(제비꼬리) → 포락선(먼 쪽이 만나는 선에서 멈춤) · 꼬임 0 · 틈 0(115-10).
|
||||
// R 25 m = 흔한 곡선(실오라기 0) · R 10 m = 비탈이 곡률 중심을 넘는 급곡선(꼬임 · 틈 0 · 실오라기 1 % 밑).
|
||||
// ④-2 직선 20 m → 좌 급곡선(R 10 m · 90°) → 직선 20 m · 높은 성토(8 m · 비탈 12 m) — 곡선 안쪽 두 직선의
|
||||
// 비탈 끝이 엇갈려 꼬이는 자리(제비꼬리) → 포락선 · 곡률 상한 뒤 꼬임 0 · 실오라기 0 · 틈 0(115-10).
|
||||
function corner(R) {
|
||||
const route = [];
|
||||
const sections = [];
|
||||
const arc = (Math.PI / 2) * R;
|
||||
const at = (c) => {
|
||||
if (c <= 20) return { x: c, y: 0, h: 0 };
|
||||
if (c <= 20 + arc) {
|
||||
const a = (c - 20) / R;
|
||||
return { x: 20 + R * Math.sin(a), y: R - R * Math.cos(a), h: a };
|
||||
}
|
||||
return { x: 20 + R, y: R + (c - 20 - arc), h: Math.PI / 2 };
|
||||
};
|
||||
const length = 40 + arc;
|
||||
for (let c = 0; c <= length + 1e-9; c += 1) {
|
||||
const p = at(c);
|
||||
route.push({ x: p.x, y: p.y });
|
||||
}
|
||||
for (let c = 0; c <= length + 1e-9; c += 10) {
|
||||
const p = at(c);
|
||||
sections.push(station(c, p.x, p.y, -Math.sin(p.h), Math.cos(p.h), 92, 1.5));
|
||||
}
|
||||
const top = 2 * R + 45;
|
||||
const bounds = { x: [-30, top], y: [-35, top], z: [80, 110] };
|
||||
const { out } = run({
|
||||
sections,
|
||||
route,
|
||||
terrainSpec: { bounds, x0: -30, x1: top, y0: -35, y1: top, step: 1, heightAt: () => 92 },
|
||||
});
|
||||
const gap = gapEdges(trianglesOf(out.root), out, frameOf(bounds, -30, top, -35, top));
|
||||
return {
|
||||
stats: out.stats,
|
||||
toeCross: toeCrossings(out),
|
||||
aspect: aspectOf(out),
|
||||
gaps: gap.gaps,
|
||||
};
|
||||
}
|
||||
results.bend = corner(25);
|
||||
results.hairpin = corner(10);
|
||||
|
||||
// ⑤ 소단 단면 — 측점 0 · 40 소단(1:1.5 로 4 m · 소단 1.5 m · 다시 1:1.5) · 20 은 소단 없음 → 꺾임 종방향 선 · 틈 0.
|
||||
{
|
||||
const bounds = { x: [-10, 50], y: [-30, 30], z: [85, 110] };
|
||||
@@ -578,7 +677,44 @@ function daylightPoints(out, bounds) {
|
||||
}
|
||||
}
|
||||
const gap = gapEdges(trianglesOf(out.root), out, frameOf(bounds, -10, 50, -30, 30));
|
||||
results.berm = { stats: out.stats, sectionErr: r6(sectionErr), gaps: gap.gaps, shade: shadeOf(out) };
|
||||
results.berm = {
|
||||
stats: out.stats,
|
||||
sectionErr: r6(sectionErr),
|
||||
gaps: gap.gaps,
|
||||
shade: shadeOf(out),
|
||||
aspect: aspectOf(out),
|
||||
};
|
||||
}
|
||||
|
||||
// ⑤-2 Taubin 면 평활화 — 고정점(노견 끝 · 비탈 끝 · 꺾임) 그대로 · 잡음 줄어듦 · 평균 높이 안 꺼짐.
|
||||
{
|
||||
const rows = [];
|
||||
let seed = 7;
|
||||
const noise = () => ((seed = (seed * 16807) % 2147483647) / 2147483647 - 0.5) * 0.4;
|
||||
for (let k = 0; k < 30; k += 1) {
|
||||
const u = Float64Array.from({ length: 11 }, (_, j) => j / 10);
|
||||
const z = Float64Array.from(u, (x) => 100 - 6 * x + (x > 0 && x < 1 ? noise() : 0));
|
||||
const fixed = Uint8Array.from(u, (_, j) => (j === 0 || j === 10 || j === 5 ? 1 : 0));
|
||||
rows.push({ u, z, fixed });
|
||||
}
|
||||
const before = rows.map((row) => Float64Array.from(row.z));
|
||||
const rough = (zs) => {
|
||||
let sum = 0;
|
||||
zs.forEach((z, k) => z.forEach((v, j) => (sum += (v - (100 - 0.6 * j)) ** 2)));
|
||||
return sum;
|
||||
};
|
||||
taubin(rows);
|
||||
let fixedMoved = 0;
|
||||
rows.forEach((row, k) =>
|
||||
row.fixed.forEach((f, j) => (fixedMoved += f && row.z[j] !== before[k][j] ? 1 : 0)),
|
||||
);
|
||||
const mean = (zs) => zs.reduce((a, z) => a + z.reduce((b, v) => b + v, 0), 0);
|
||||
results.taubin = {
|
||||
fixedMoved,
|
||||
roughBefore: r6(rough(before)),
|
||||
roughAfter: r6(rough(rows.map((row) => row.z))),
|
||||
meanShift: r6(Math.abs(mean(rows.map((row) => row.z)) - mean(before)) / (30 * 11)),
|
||||
};
|
||||
}
|
||||
|
||||
// ⑥ 3D 방식 등록부 — 「제안 ③」 자리 · 등록.
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 115-10 — B05 3D 제안 ③ 사면 모양 평활화 검사.
|
||||
|
||||
도우미(helper_115_8_corridor_loft.cjs)를 함께 쓴다. 사면 격자(실오라기 없음) · 비탈 끝 선
|
||||
꼬임 0(포락선) · Taubin 평활화(고정점 그대로 · 줄어들지 않음) · 측점 단면 = B06 · 이음 틈 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_8_corridor_loft.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=240,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_slope_grid_no_slivers(results):
|
||||
# 직선 · 소단 — 사면 삼각형 가로세로비 10 넘는 것 0(행마다 점 둘 실오라기 부채꼴 없음)
|
||||
for name in ("straight", "berm"):
|
||||
aspect = results[name]["aspect"]
|
||||
assert aspect["count"] > 1000 and aspect["over"] == 0, (name, aspect)
|
||||
|
||||
|
||||
def test_curves_few_slivers(results):
|
||||
# 곡선 안쪽(행이 모이는 자리) · 두 비탈이 만나는 골 둘레만 조금 — 1.5 % 밑
|
||||
for name in ("curve", "bend", "hairpin"):
|
||||
aspect = results[name]["aspect"]
|
||||
assert aspect["over"] <= 0.015 * aspect["count"], (name, aspect)
|
||||
|
||||
|
||||
def test_toe_line_no_self_crossing(results):
|
||||
for name in ("curve", "bend", "hairpin"):
|
||||
assert results[name]["toeCross"] == 0, name
|
||||
# 급곡선(R 10 m) — 엇갈린 사면 선은 만나는 선(골)에서 멈춤
|
||||
assert results["hairpin"]["stats"]["bowtie"] > 0
|
||||
|
||||
|
||||
def test_no_gap_no_failed_constraint(results):
|
||||
for name in ("straight", "flip", "curve", "bend", "hairpin", "berm"):
|
||||
assert results[name]["gaps"] == 0, name
|
||||
assert results[name]["stats"]["failed"] == 0, name
|
||||
|
||||
|
||||
def test_fixed_edges_and_station_section(results):
|
||||
r = results["straight"]
|
||||
assert r["drapeErr"] < 1e-4 # 비탈 끝 = 지형 위(고정)
|
||||
assert r["sectionErr"] < 1e-4 and r["offsetErr"] < 1e-4 # 측점 단면 · 비탈 끝 = B06
|
||||
assert results["berm"]["sectionErr"] < 1e-4
|
||||
|
||||
|
||||
def test_taubin_keeps_fixed_and_volume(results):
|
||||
r = results["taubin"]
|
||||
assert r["fixedMoved"] == 0 # 노견 끝 · 비탈 끝 · 꺾임 그대로
|
||||
assert r["roughAfter"] < 0.5 * r["roughBefore"] # 잡음 반 넘게 줄어듦
|
||||
assert r["meanShift"] < 0.01 # 평균 높이 안 꺼짐(Taubin)
|
||||
@@ -30,10 +30,10 @@ def results():
|
||||
|
||||
def test_between_stations_daylight_monotone_cubic(results):
|
||||
r = results["straight"]
|
||||
assert r["rowsChecked"] > 300
|
||||
assert r["rowsChecked"] > 200 # 비탈 끝 점(0.3 m 밑은 솎음 · 115-10)
|
||||
assert r["offsetErr"] < 1e-4 # 측점(0 · 20 · 40)에서 비탈 끝 = B06 오프셋 6 · 9 · 6
|
||||
assert r["overshoot"] < 1e-6 # 사이는 단조 3차 — 두 측점 값 밖으로 안 넘침(115-9)
|
||||
assert r["kink"] < 0.05 # 꼭대기(측점 20) 양옆 기울기 차 — 직선 보간이면 0.3
|
||||
assert r["kink"] < 0.1 # 꼭대기(측점 20) 양옆 기울기 차 — 직선 보간이면 0.3
|
||||
|
||||
|
||||
def test_station_section_matches_b06(results):
|
||||
@@ -63,7 +63,8 @@ def test_fill_cut_change_breaks_like_plan(results):
|
||||
|
||||
def test_curve_bowtie_cleared(results):
|
||||
r = results["curve"]
|
||||
assert r["stats"]["bowtie"] > 0 and r["selfCross"] == 0
|
||||
# 115-10 — 곡선 안쪽은 곡률 상한 · 포락선으로(자르지 않음) → 둘레 · 비탈 끝 선 꼬임 0
|
||||
assert r["selfCross"] == 0 and r["toeCross"] == 0
|
||||
|
||||
|
||||
def test_pairing_bends(results):
|
||||
|
||||
Reference in New Issue
Block a user