From e5d8002c7952e20ff282263a785487e82ac1af92 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 27 Oct 2026 15:16:49 +0900 Subject: [PATCH] =?UTF-8?q?fix(view):=20=EC=A7=80=ED=98=95=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EC=9D=B4=20=EC=97=86=EC=9D=84=20=EB=95=8C=2060=20m=20?= =?UTF-8?q?=EC=82=AC=EA=B0=81=ED=8C=90=20=EB=8C=80=EC=8B=A0=20=EA=B5=90?= =?UTF-8?q?=EA=B0=81=EB=B3=84=20=EC=A7=80=EB=B0=98=EA=B3=A0=20=EC=84=A0=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지형 격자가 없으면 교각 지반고 위치에 얇은 선(코핑 길이 + 여유 2 m)만 그림 - 전체 보기에서도 지형이 없으면 교각별 지반고 선을 표시, 지형이 있으면 공유 격자만 표시 - 부재 표시 '지반' 체크박스는 두 경우 모두 동작 - 테스트: 얇은 선 위치와 길이, 지형 있을 때 선 미표시 Co-Authored-By: Claude Haiku 5.5 --- src/main.ts | 3 ++- src/view/all-piers.ts | 5 +++-- src/view/scene.ts | 14 ++++++++------ tests/all-piers.test.ts | 16 ++++++++++------ tests/ground-marker.test.ts | 38 +++++++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 tests/ground-marker.test.ts diff --git a/src/main.ts b/src/main.ts index 84c5e8d..85155ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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; diff --git a/src/view/all-piers.ts b/src/view/all-piers.ts index 4d4004d..8250bb1 100644 --- a/src/view/all-piers.ts +++ b/src/view/all-piers.ts @@ -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; diff --git a/src/view/scene.ts b/src/view/scene.ts index 30a209f..e94579e 100644 --- a/src/view/scene.ts +++ b/src/view/scene.ts @@ -20,6 +20,9 @@ export const PART_LABELS: Record = { 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 = { 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(); diff --git a/tests/all-piers.test.ts b/tests/all-piers.test.ts index c09da9a..5b6ac0b 100644 --- a/tests/all-piers.test.ts +++ b/tests/all-piers.test.ts @@ -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(); }); }); diff --git a/tests/ground-marker.test.ts b/tests/ground-marker.test.ts new file mode 100644 index 0000000..ba7fe79 --- /dev/null +++ b/tests/ground-marker.test.ts @@ -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'); + }); +});