From 844177b40e3e5021ea185259a9e189369e858676 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 27 Oct 2026 15:23:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(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=20=EA=B5=90?= =?UTF-8?q?=EA=B0=81=EB=B3=84=20=EC=A7=80=EB=B0=98=20=ED=8C=A8=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EC=82=AC=EA=B0=81=ED=98=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=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/view/scene.ts | 14 ++++++++------ tests/ground-marker.test.ts | 27 ++++++++++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/view/scene.ts b/src/view/scene.ts index e94579e..e0d7c5d 100644 --- a/src/view/scene.ts +++ b/src/view/scene.ts @@ -20,8 +20,8 @@ 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; +/** Ground pad drawn without a terrain grid (m): a square of side = coping length + margin, thin. */ +const GROUND_PAD = { margin: 2, thickness: 0.05 } as const; const COLORS: Record = { coping: 0xb8c2cc, @@ -177,15 +177,17 @@ export function buildPierScene(m: PierModel, terrain?: TerrainGrid): PierScene { } // 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). + // Without a terrain, a thin square pad at this pier's 지반고, side = coping length + 2 m (승인 2026-10-27). + // It is a local estimate, not a terrain surface: the pads are not joined to each other. 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 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 side = m.copingL + GROUND_PAD.margin; + const pad = new THREE.BoxGeometry(side, GROUND_PAD.thickness, side); + pad.translate(m.copingL / 2, m.groundEl, 0); + add('ground', pad, 0.35); } const root = new THREE.Group(); diff --git a/tests/ground-marker.test.ts b/tests/ground-marker.test.ts index ba7fe79..d1ce7f6 100644 --- a/tests/ground-marker.test.ts +++ b/tests/ground-marker.test.ts @@ -10,18 +10,27 @@ 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', () => { + it('draws a thin square pad at the pier 지반고, side = coping length + 2 m', () => { const scene = buildPierScene(model); + const mesh = scene.parts.ground.children[0] as THREE.Mesh; + mesh.updateMatrixWorld(true); + const pos = mesh.geometry.getAttribute('position'); + const pts: THREE.Vector3[] = []; + for (let i = 0; i < pos.count; i++) pts.push(new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(mesh.matrixWorld)); + const top = Math.max(...pts.map((p) => p.y)); + const corners = pts.filter((p) => Math.abs(p.y - top) < 1e-6); + // Four corners of the top face, with a distinct set of positions. + const unique = corners.filter((p, i) => corners.findIndex((q) => q.distanceTo(p) < 1e-6) === i); + expect(unique).toHaveLength(4); + const side = model.copingL + 2; + const edges = unique.flatMap((a, i) => unique.slice(i + 1).map((b) => a.distanceTo(b))).sort((a, b) => a - b); + // Four sides of length `side`, two diagonals of length side·√2. + for (const e of edges.slice(0, 4)) expect(e).toBeCloseTo(side, 3); + for (const e of edges.slice(4)) expect(e).toBeCloseTo(side * Math.SQRT2, 3); + // Thin: a pad, not a solid block. Centred on the ground EL. 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.getSize(new THREE.Vector3()).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', () => {