172 lines
6.7 KiB
TypeScript
172 lines
6.7 KiB
TypeScript
/* =============================================================================
|
|
* E04_Stab3D_Mesh.ts
|
|
* E04 3D 보기 메쉬(PLAN 533-3) — 합성 지표(원지형 + 노선면) 격자 메쉬 · 임계 활동면(구 · 타원체를
|
|
* 지표 아래만 남긴 잘린 면) · 지표 높이 보간. 좌표 = 모델(x 동 · y 북 · z 위) 그대로 —
|
|
* 화면 좌표 변환은 화면 파일 몫. 순수 함수만(DOM · three 금지).
|
|
* ========================================================================== */
|
|
|
|
import type { Grid, SlipBest } from "./E04_Stab3D_Raster";
|
|
|
|
export interface MeshArrays {
|
|
/** 꼭짓점 [x, y, z, …] */
|
|
positions: Float32Array;
|
|
/** 삼각형 꼭짓점 번호 */
|
|
indices: Uint32Array;
|
|
/** 꼭짓점마다 래스터 셀 번호(색칠 · 툴팁용) */
|
|
cells: Int32Array;
|
|
}
|
|
|
|
/** 격자 위 값이 있는 꼭짓점만 이어 메쉬 — 네 모서리 중 셋 이상 있으면 삼각형 */
|
|
function gridMesh(grid: Grid, height: (index: number) => number | null): MeshArrays {
|
|
const vertexOf = new Int32Array(grid.nx * grid.ny).fill(-1);
|
|
const positions: number[] = [];
|
|
const cells: number[] = [];
|
|
for (let iy = 0; iy < grid.ny; iy += 1) {
|
|
for (let ix = 0; ix < grid.nx; ix += 1) {
|
|
const index = iy * grid.nx + ix;
|
|
const z = height(index);
|
|
if (z === null) continue;
|
|
vertexOf[index] = cells.length;
|
|
positions.push(grid.x0 + ix * grid.dx, grid.y0 + iy * grid.dx, z);
|
|
cells.push(index);
|
|
}
|
|
}
|
|
const indices: number[] = [];
|
|
for (let iy = 0; iy + 1 < grid.ny; iy += 1) {
|
|
for (let ix = 0; ix + 1 < grid.nx; ix += 1) {
|
|
const a = vertexOf[iy * grid.nx + ix];
|
|
const b = vertexOf[iy * grid.nx + ix + 1];
|
|
const c = vertexOf[(iy + 1) * grid.nx + ix + 1];
|
|
const d = vertexOf[(iy + 1) * grid.nx + ix];
|
|
// 반시계(위에서 봄) — a b c · a c d
|
|
if (a >= 0 && b >= 0 && c >= 0 && d >= 0) indices.push(a, b, c, a, c, d);
|
|
else if (a >= 0 && b >= 0 && c >= 0) indices.push(a, b, c);
|
|
else if (a >= 0 && c >= 0 && d >= 0) indices.push(a, c, d);
|
|
else if (a >= 0 && b >= 0 && d >= 0) indices.push(a, b, d);
|
|
else if (b >= 0 && c >= 0 && d >= 0) indices.push(b, c, d);
|
|
}
|
|
}
|
|
return {
|
|
positions: Float32Array.from(positions),
|
|
indices: Uint32Array.from(indices),
|
|
cells: Int32Array.from(cells),
|
|
};
|
|
}
|
|
|
|
/** 합성 지표 메쉬 — z 가 null 인 셀은 뺌 */
|
|
export function terrainMesh(grid: Grid, z: readonly (number | null)[]): MeshArrays {
|
|
return gridMesh(grid, (index) => z[index]);
|
|
}
|
|
|
|
/** 활동면 깊이 — 구 · 타원체 아래 반쪽 높이(밖이면 null) */
|
|
export function slipHeight(best: SlipBest, x: number, y: number): number | null {
|
|
const az = (best.azimuth_deg * Math.PI) / 180;
|
|
const dx = x - best.x;
|
|
const dy = y - best.y;
|
|
// 활동 방향 성분 · 회전축(수직) 성분 — 축 방향만 ratio 로 나눔
|
|
const along = dx * Math.sin(az) + dy * Math.cos(az);
|
|
const axis = (dx * Math.cos(az) - dy * Math.sin(az)) / Math.max(best.ratio, 1e-9);
|
|
const rest = best.r * best.r - along * along - axis * axis;
|
|
return rest < 0 ? null : best.z - Math.sqrt(rest);
|
|
}
|
|
|
|
/** 잘린 셀 → 활동면 높이(활동면이 지표보다 낮은 셀만 · 못 뚫는 층 윗면 아래로는 안 내려감 · 엔진 덩이 셀만) */
|
|
export function cutCells(
|
|
grid: Grid,
|
|
z: readonly (number | null)[],
|
|
best: SlipBest,
|
|
): Map<number, number> {
|
|
const floor = new Map(best.floor ?? []);
|
|
const mass = best.cells ? new Set(best.cells) : null;
|
|
const out = new Map<number, number>();
|
|
z.forEach((top, index) => {
|
|
if (top === null || (mass && !mass.has(index))) return;
|
|
const x = grid.x0 + (index % grid.nx) * grid.dx;
|
|
const y = grid.y0 + Math.floor(index / grid.nx) * grid.dx;
|
|
const slip = slipHeight(best, x, y);
|
|
const base = Math.max(slip ?? top, floor.get(index) ?? -Infinity);
|
|
// 엔진 덩이 셀 = 그대로(셀 안 일부만 잘린 가장자리 = 두께 0) · 옛 저장본 = 셀 중심이 잘린 칸만
|
|
if (mass) out.set(index, Math.min(base, top));
|
|
else if (base < top) out.set(index, base);
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/** 임계 활동체 평면 중심 = 잘린 셀 중심의 평균(없으면 구 중심) — 지도 최소 표시 자리 */
|
|
export function footprintCenter(
|
|
grid: Grid,
|
|
cut: ReadonlyMap<number, number>,
|
|
best: SlipBest,
|
|
): { x: number; y: number } {
|
|
if (!cut.size) return { x: best.x, y: best.y };
|
|
let sx = 0;
|
|
let sy = 0;
|
|
for (const index of cut.keys()) {
|
|
sx += grid.x0 + (index % grid.nx) * grid.dx;
|
|
sy += grid.y0 + Math.floor(index / grid.nx) * grid.dx;
|
|
}
|
|
return { x: sx / cut.size, y: sy / cut.size };
|
|
}
|
|
|
|
/** 잘린 셀의 8 이웃 중 잘리지 않은 셀(값 있음) — 활동면 테두리 */
|
|
export function rimCells(
|
|
grid: Grid,
|
|
z: readonly (number | null)[],
|
|
cut: ReadonlyMap<number, number>,
|
|
): Set<number> {
|
|
const rim = new Set<number>();
|
|
for (const index of cut.keys()) {
|
|
const ix = index % grid.nx;
|
|
const iy = Math.floor(index / grid.nx);
|
|
for (let dy = -1; dy <= 1; dy += 1) {
|
|
for (let dx = -1; dx <= 1; dx += 1) {
|
|
const jx = ix + dx;
|
|
const jy = iy + dy;
|
|
if (jx < 0 || jy < 0 || jx >= grid.nx || jy >= grid.ny) continue;
|
|
const next = jy * grid.nx + jx;
|
|
if (!cut.has(next) && z[next] !== null) rim.add(next);
|
|
}
|
|
}
|
|
}
|
|
return rim;
|
|
}
|
|
|
|
/** 임계 활동면 = 잘린 셀(활동면 높이) + 테두리 셀(지표 높이) — 지표 구멍(잘린 셀)을 틈 없이 메움 */
|
|
export function slipMesh(grid: Grid, z: readonly (number | null)[], best: SlipBest): MeshArrays {
|
|
const cut = cutCells(grid, z, best);
|
|
const rim = rimCells(grid, z, cut);
|
|
return gridMesh(grid, (index) => cut.get(index) ?? (rim.has(index) ? z[index] : null));
|
|
}
|
|
|
|
/** 지표에서 잘린 셀을 뺀 메쉬(활동체를 걷어낸 모습) */
|
|
export function terrainWithout(
|
|
grid: Grid,
|
|
z: readonly (number | null)[],
|
|
cut: ReadonlyMap<number, number>,
|
|
): MeshArrays {
|
|
return gridMesh(grid, (index) => (cut.has(index) ? null : z[index]));
|
|
}
|
|
|
|
/** 지표 높이 쌍선형 보간(격자 밖 · 빈 셀 = null) */
|
|
export function heightAt(
|
|
grid: Grid,
|
|
z: readonly (number | null)[],
|
|
x: number,
|
|
y: number,
|
|
): number | null {
|
|
const fx = (x - grid.x0) / grid.dx;
|
|
const fy = (y - grid.y0) / grid.dx;
|
|
const ix = Math.floor(fx);
|
|
const iy = Math.floor(fy);
|
|
if (ix < 0 || iy < 0 || ix + 1 >= grid.nx || iy + 1 >= grid.ny) return null;
|
|
const at = (i: number, j: number): number | null => z[j * grid.nx + i];
|
|
const z00 = at(ix, iy);
|
|
const z10 = at(ix + 1, iy);
|
|
const z01 = at(ix, iy + 1);
|
|
const z11 = at(ix + 1, iy + 1);
|
|
if (z00 === null || z10 === null || z01 === null || z11 === null) return null;
|
|
const tx = fx - ix;
|
|
const ty = fy - iy;
|
|
return (z00 * (1 - tx) + z10 * tx) * (1 - ty) + (z01 * (1 - tx) + z11 * tx) * ty;
|
|
}
|