feat(ground): draw the terrain grid around the piers in the 3D view
- ground-model.ts: read schema 2 terrain_grid (origin, step, nx, ny, z), checked the same way as the stored project form; schema 1 is still read. - view/terrain.ts: the grid as a mesh in pier-scene coordinates (x = E − pivot E, z = −(N − pivot N), y = EL), same as the pier frame. Cells with a missing corner are not drawn. - scene.ts: draws the terrain in place of the 60 m plane when the model has a grid; the camera fits the members, not the terrain. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
be76425c8f
commit
bfd41d7c8d
6 files changed
+200
-16
No files matched your search
@@ -3,8 +3,18 @@
|
||||
* 앱은 교각 중심의 지반고만 쓴다. 적용은 사용자가 누를 때만 하며, 자동으로 입력을 바꾸지 않는다.
|
||||
*/
|
||||
|
||||
/** Must match GROUND_MODEL_SCHEMA in the Python package. */
|
||||
export const GROUND_MODEL_SCHEMA = 1;
|
||||
/** Must match GROUND_MODEL_SCHEMA in the Python package. Schema 1 (no terrain grid) is still read. */
|
||||
export const GROUND_MODEL_SCHEMA = 2;
|
||||
const READABLE_SCHEMAS = [1, GROUND_MODEL_SCHEMA];
|
||||
|
||||
/** Terrain sampled on a regular grid (측량 X · Y, m; EL m). z runs over Y rows, X columns; null = no data. */
|
||||
export interface TerrainGrid {
|
||||
origin: [number, number];
|
||||
step: number;
|
||||
nx: number;
|
||||
ny: number;
|
||||
z: (number | null)[];
|
||||
}
|
||||
|
||||
export interface GroundModelPier {
|
||||
/** 교각 이름 (입력의 name, 예: P1). */
|
||||
@@ -19,6 +29,8 @@ export interface GroundModelFile {
|
||||
schema: number;
|
||||
generated: string;
|
||||
piers: GroundModelPier[];
|
||||
/** 교각 주변 지형 격자 (schema 2). Absent in schema 1 files. */
|
||||
terrainGrid?: TerrainGrid;
|
||||
}
|
||||
|
||||
function numOrNull(v: unknown): number | null {
|
||||
@@ -34,8 +46,8 @@ function numOrNull(v: unknown): number | null {
|
||||
export function checkGroundModel(raw: unknown): GroundModelFile {
|
||||
const obj = raw as Partial<GroundModelFile> | null;
|
||||
if (!obj || typeof obj !== 'object') throw new Error('지반 모델 파일 형식이 아닙니다.');
|
||||
if (obj.schema !== undefined && obj.schema !== GROUND_MODEL_SCHEMA) {
|
||||
throw new Error(`지반 모델 형식 번호 ${String(obj.schema)}을(를) 읽을 수 없습니다 (지원: ${GROUND_MODEL_SCHEMA}).`);
|
||||
if (obj.schema !== undefined && !READABLE_SCHEMAS.includes(obj.schema)) {
|
||||
throw new Error(`지반 모델 형식 번호 ${String(obj.schema)}을(를) 읽을 수 없습니다 (지원: ${READABLE_SCHEMAS.join(', ')}).`);
|
||||
}
|
||||
if (!Array.isArray(obj.piers)) throw new Error('지반 모델 파일에 교각 목록(piers)이 없습니다.');
|
||||
const piers = obj.piers.map((p, i) => {
|
||||
@@ -45,7 +57,29 @@ export function checkGroundModel(raw: unknown): GroundModelFile {
|
||||
if (!ground || typeof ground !== 'object') throw new Error(`지반 모델 ${id}: 지반 값(ground)이 없습니다.`);
|
||||
return { id, centerEl: numOrNull(ground.center), excelEl: numOrNull(ground.excel) };
|
||||
});
|
||||
return { schema: GROUND_MODEL_SCHEMA, generated: typeof obj.generated === 'string' ? obj.generated : '', piers };
|
||||
const rawGrid = (raw as { terrain_grid?: unknown }).terrain_grid;
|
||||
const terrainGrid = rawGrid === undefined ? undefined : checkTerrainGrid(rawGrid);
|
||||
return {
|
||||
schema: GROUND_MODEL_SCHEMA,
|
||||
generated: typeof obj.generated === 'string' ? obj.generated : '',
|
||||
piers,
|
||||
...(terrainGrid ? { terrainGrid } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks a terrain grid: origin, step, the size, and one value (or null) per node. */
|
||||
export function checkTerrainGrid(raw: unknown): TerrainGrid {
|
||||
const g = raw as Partial<TerrainGrid> | null;
|
||||
const finite = (v: unknown) => typeof v === 'number' && Number.isFinite(v);
|
||||
if (!g || typeof g !== 'object') throw new Error('지형 격자(terrain_grid) 형식이 맞지 않습니다.');
|
||||
if (!Array.isArray(g.origin) || g.origin.length !== 2 || !g.origin.every(finite)) throw new Error('지형 격자의 기준점(origin)이 맞지 않습니다.');
|
||||
if (!finite(g.step) || !(g.step as number > 0)) throw new Error('지형 격자 간격(step)이 맞지 않습니다.');
|
||||
if (!Number.isInteger(g.nx) || !Number.isInteger(g.ny) || (g.nx as number) < 2 || (g.ny as number) < 2) {
|
||||
throw new Error('지형 격자 크기(nx, ny)가 맞지 않습니다.');
|
||||
}
|
||||
if (!Array.isArray(g.z) || g.z.length !== (g.nx as number) * (g.ny as number)) throw new Error('지형 격자 값의 개수가 nx × ny와 다릅니다.');
|
||||
if (!g.z.every((v) => v === null || finite(v))) throw new Error('지형 격자 값에 숫자가 아닌 것이 있습니다.');
|
||||
return { origin: [g.origin[0], g.origin[1]], step: g.step as number, nx: g.nx as number, ny: g.ny as number, z: g.z as (number | null)[] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +88,7 @@ export function checkGroundModel(raw: unknown): GroundModelFile {
|
||||
*/
|
||||
export function checkStoredGroundModel(raw: unknown): GroundModelFile {
|
||||
const obj = raw as Partial<GroundModelFile> | null;
|
||||
if (!obj || typeof obj !== 'object' || obj.schema !== GROUND_MODEL_SCHEMA) {
|
||||
if (!obj || typeof obj !== 'object' || !READABLE_SCHEMAS.includes(obj.schema as number)) {
|
||||
throw new Error('프로젝트 안의 지반 모델 형식 번호가 맞지 않습니다.');
|
||||
}
|
||||
if (!Array.isArray(obj.piers)) throw new Error('프로젝트 안의 지반 모델에 교각 목록이 없습니다.');
|
||||
@@ -62,6 +96,7 @@ export function checkStoredGroundModel(raw: unknown): GroundModelFile {
|
||||
const ok = typeof p?.id === 'string' && (p.centerEl === null || typeof p.centerEl === 'number') && (p.excelEl === null || typeof p.excelEl === 'number');
|
||||
if (!ok) throw new Error(`프로젝트 안의 지반 모델 교각 ${String(p?.id)}의 값이 맞지 않습니다.`);
|
||||
}
|
||||
if (obj.terrainGrid !== undefined) checkTerrainGrid(obj.terrainGrid);
|
||||
return obj as GroundModelFile;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user