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:
ypshimandClaude Haiku 5.5 committed 2026-10-23 17:57:30 +09:00
1 parent be76425c8f
commit bfd41d7c8d
6 files changed
+200 -16

No files matched your search

+48 -2
View File
@@ -30,7 +30,7 @@ describe('지반 모델 읽기 (ground_model.json, schema 1)', () => {
});
it('refuses another schema number instead of guessing the layout', () => {
expect(() => parseGroundModel(SAMPLE.replace('"schema":1', '"schema":2'))).toThrow('형식 번호 2');
expect(() => parseGroundModel(SAMPLE.replace('"schema":1', '"schema":3'))).toThrow('형식 번호 3');
});
it('refuses a file without a pier list or without pier names', () => {
@@ -43,13 +43,59 @@ describe('지반 모델 읽기 (ground_model.json, schema 1)', () => {
});
});
const LEGACY_SAMPLE = JSON.stringify({ schema: 1, piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] });
describe('지형 격자 (schema 2)', () => {
const WITH_GRID = JSON.stringify({
schema: 2,
generated: '2026-10-23',
piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }],
terrain_grid: { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] },
});
it('reads the terrain grid as terrainGrid', () => {
const m = parseGroundModel(WITH_GRID);
expect(m.schema).toBe(2);
expect(m.terrainGrid).toEqual({ origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] });
});
it('a schema 1 file has no terrain grid', () => {
expect(parseGroundModel(LEGACY_SAMPLE).terrainGrid).toBeUndefined();
});
it('refuses a grid whose value count is not nx × ny', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,29]'))).toThrow('nx × ny');
});
it('refuses a grid value that is not a number or null', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,"x",29,30]'))).toThrow('숫자가 아닌');
});
it('refuses an unknown schema number', () => {
expect(() => parseGroundModel(WITH_GRID.replace('"schema":2', '"schema":3'))).toThrow('형식 번호 3');
});
it('keeps the grid in a saved project', () => {
const m = parseGroundModel(WITH_GRID);
const text = serializeProject({
schemaVersion: SCHEMA_VERSION,
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
source: { fileName: 'x.xlsx', sheets: [] },
piers: [{ id: 'P1', input: loadP1() }],
history: [],
groundModel: m,
});
expect(parseProject(text).groundModel?.terrainGrid).toEqual(m.terrainGrid);
});
});
describe('번호 없는 옛 지반 모델 (B′: 임시 경로)', () => {
// examples/out/ground_model.json as written before the schema number existed (P1–P6, same values as the report).
const LEGACY = readFileSync(new URL('./fixtures/ground-model-legacy.json', import.meta.url), 'utf-8');
it('reads the unversioned file and reports it as schema 1', () => {
const m = parseGroundModel(LEGACY);
expect(m.schema).toBe(1);
expect(m.schema).toBe(2); // normalised to the current schema
expect(m.piers.map((p) => p.id)).toEqual(['P1', 'P2', 'P3', 'P4', 'P5', 'P6']);
expect(groundElFor(m, 'P1')).toBe(29.355);
expect(m.piers[0].excelEl).toBe(28.931);