feat(B05): 3D 새 방식 구조물 마스크 — 풋프린트 · 아령 외곽 구멍 · 날개 안쪽 성토 금지 · 구간 단면 상부 포락선 + 이웃 측점 테이퍼(115-2)
- 구조물 구간 단면 = 소유 측점 실루엣 상부 포락선(앵커 기준) · 구간 안 고정 · 끝 → 이웃 측점 테이퍼 - BOX(구조물에서 끝나는 실루엣) 구간은 단면 끝 너머 연장 없음 = 본체 밖 원지반 - 지형 구멍 = 구조물 풋프린트 · BOX 아령 외곽(솔리드가 메움 · 솔리드 코드는 그대로) - 날개 안쪽 유로 · 에이프런 성토 금지 고리 — 원지반(세월교는 바닥판 상단)까지만 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
@@ -243,4 +243,182 @@ const results = {};
|
||||
};
|
||||
}
|
||||
|
||||
// ⑦ 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));
|
||||
|
||||
@@ -74,3 +74,41 @@ def test_three_copy_only_xy_fixed_hole_and_tint(results):
|
||||
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