fix(view): 지형 모델이 없을 때 60 m 사각판 대신 교각별 지반고 선 표시

- 지형 격자가 없으면 교각 지반고 위치에 얇은 선(코핑 길이 + 여유 2 m)만 그림
- 전체 보기에서도 지형이 없으면 교각별 지반고 선을 표시, 지형이 있으면 공유 격자만 표시
- 부재 표시 '지반' 체크박스는 두 경우 모두 동작
- 테스트: 얇은 선 위치와 길이, 지형 있을 때 선 미표시

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-27 15:16:49 +09:00
1 parent 765384cd71
commit e5d8002c79
5 files changed
+61 -15

No files matched your search

+2 -1
View File
@@ -324,8 +324,9 @@ function partScenes(): PierScene[] {
function setPartVisible(part: PartName, on: boolean): void {
if (part === 'ground' && allPiers) {
// Per-pier ground planes stay hidden in 전체 보기; only the shared terrain follows the checkbox.
// With terrain only the shared grid follows the checkbox; without it each pier's 지반고 line does.
if (allPiers.terrain) allPiers.terrain.visible = on;
else for (const s of allPiers.scenes.values()) s.parts.ground.visible = on;
return;
}
for (const s of partScenes()) s.parts[part].visible = on;
+3 -2
View File
@@ -25,7 +25,7 @@ export interface AllPiers {
/**
* Builds the concrete members of several piers in one frame (전체 보기, 콘크리트만: no rebar). The first entry is the
* reference. Each pier's own ground plane is hidden; the shared terrain grid is drawn once.
* reference. With a terrain grid it is drawn once and the per-pier ground lines are hidden.
*/
export function buildAllPiers(entries: { name: string; model: PierModel }[], terrain?: TerrainGrid): AllPiers {
const piers = new THREE.Group();
@@ -33,7 +33,8 @@ export function buildAllPiers(entries: { name: string; model: PierModel }[], ter
const reference = entries[0].model.pivot;
for (const { name, model } of entries) {
const scene = buildPierScene(model);
scene.parts.ground.visible = false;
// Without terrain each pier keeps its own 지반고 line; with terrain the shared grid replaces them.
scene.parts.ground.visible = terrain === undefined;
const { x, z } = pierOffset(model.pivot, reference);
scene.root.position.set(x, 0, z);
scene.root.userData.pier = name;
+8 -6
View File
@@ -20,6 +20,9 @@ export const PART_LABELS: Record<PartName, string> = {
ground: '지반',
};
/** Ground-level line drawn without a terrain grid (m): length = coping + margin, thin and narrow. */
const GROUND_LINE = { margin: 2, thickness: 0.05, width: 0.15 } as const;
const COLORS: Record<PartName, number> = {
coping: 0xb8c2cc,
columns: 0x9aa7b4,
@@ -173,17 +176,16 @@ export function buildPierScene(m: PierModel, terrain?: TerrainGrid): PierScene {
}
}
// Ground: the terrain grid around the piers when the ground model has one (수 m 떨어진 지형까지 보임),
// otherwise a translucent plane at the ground EL, used only to judge soil cover (not a computed input).
// Ground: the terrain grid around the piers when the ground model has one (수 m 떨어진 지형까지 보임).
// Without a terrain, only a thin line at this pier's 지반고 across the coping (no surface is assumed, 승인 2026-10-27).
if (terrain) {
const material = new THREE.MeshStandardMaterial({ color: COLORS.ground, roughness: 0.9, metalness: 0, transparent: true, opacity: 0.35, side: THREE.DoubleSide });
material.userData.baseOpacity = 0.35;
parts.ground.add(new THREE.Mesh(terrainGeometry(terrain, m.pivot), material));
} else {
const ground = new THREE.PlaneGeometry(60, 60);
ground.rotateX(-Math.PI / 2);
ground.translate(m.copingL / 2, m.groundEl, 0);
add('ground', ground, 0.35);
const line = new THREE.BoxGeometry(m.copingL + GROUND_LINE.margin, GROUND_LINE.thickness, GROUND_LINE.width);
line.translate(m.copingL / 2, m.groundEl, 0);
add('ground', line, 1);
}
const root = new THREE.Group();
+10 -6
View File
@@ -31,11 +31,15 @@ describe('buildAllPiers', () => {
expect(b.max.distanceTo(a.max)).toBeLessThan(1e-4);
});
it('hides each pier ground plane and keys scenes by name', () => {
const all = buildAllPiers([{ name: 'P1', model }]);
expect([...all.scenes.keys()]).toEqual(['P1']);
expect(all.scenes.get('P1')!.parts.ground.visible).toBe(false);
expect(all.terrain).toBeNull();
expect(all.view.size).toBeGreaterThan(0);
it('keeps each pier 지반고 line without terrain, and hides it when the shared terrain is drawn', () => {
const without = buildAllPiers([{ name: 'P1', model }]);
expect([...without.scenes.keys()]).toEqual(['P1']);
expect(without.scenes.get('P1')!.parts.ground.visible).toBe(true);
expect(without.terrain).toBeNull();
expect(without.view.size).toBeGreaterThan(0);
const withTerrain = buildAllPiers([{ name: 'P1', model }], { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [30, 31, 32, 33] });
expect(withTerrain.scenes.get('P1')!.parts.ground.visible).toBe(false);
expect(withTerrain.terrain).not.toBeNull();
});
});
+38
View File
@@ -0,0 +1,38 @@
import * as THREE from 'three';
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import type { TerrainGrid } from '../src/core/ground-model';
import { buildPierScene } from '../src/view/scene';
import { loadP1 } from './helpers';
const model = buildPier(loadP1());
const terrain: TerrainGrid = { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [30, 31, 32, 33] };
describe('지반 표시 (지형 모델이 없을 때)', () => {
it('draws a thin ground-level line at the pier 지반고, not a 60 m square', () => {
const scene = buildPierScene(model);
const box = new THREE.Box3().setFromObject(scene.parts.ground);
const size = box.getSize(new THREE.Vector3());
// Thin: a line, not a plane. Centred on the ground EL.
expect(size.y).toBeLessThan(0.1);
expect(box.getCenter(new THREE.Vector3()).y).toBeCloseTo(model.groundEl, 2);
// Spans the coping along the pier, plus a small margin. The line may be rotated to the 교축, so measure its
// horizontal extent as a diagonal of the box's x and z extents.
const plan = Math.hypot(size.x, size.z);
expect(plan).toBeGreaterThan(model.copingL);
expect(plan).toBeLessThan(model.copingL + 3);
});
it('keeps the ground group out of the pier size', () => {
const scene = buildPierScene(model);
expect(scene.size).toBeLessThan(60);
});
it('does not draw the line when the terrain grid is present (the terrain replaces it)', () => {
const scene = buildPierScene(model, terrain);
expect(scene.parts.ground.children.length).toBe(1);
const mesh = scene.parts.ground.children[0] as THREE.Mesh;
expect(mesh.geometry.type).toBe('BufferGeometry');
});
});