feat(viewer3d): 2단계 날개벽 치수선 수평화, 사각형 테이블 제거, 치수 표기 간소화(L=00.0m, H=00.0m) 및 흰색 적용

This commit is contained in:
Antigravity Agent committed 2026-10-22 16:35:05 +09:00
1 parent 90a178a7e5
commit ec50cf733c
1 file changed
+106 -49
+106 -49
View File
@@ -682,8 +682,8 @@ class Viewer3D {
// 암거본체 선택 시 날개벽 구조물의 이름만 표시 (Requirement 2.2.2 & 3.2.2)
sp.visible = true;
} else {
// 특정 날개벽 선택 시 해당 날개벽 이름만 표시
sp.visible = (sp.userData.wingwallId === currentFocus);
// 특정 날개벽 선택 시: 2단계에서는 치수선 가독성을 위해 숨김, 3단계에서는 선택된 날개벽 이름 표시
sp.visible = (this.stepMode === 3 && sp.userData.wingwallId === currentFocus);
}
}
}
@@ -758,7 +758,7 @@ class Viewer3D {
this.updateWingwallNameSpritesVisibility();
}
// Requirement 3: 선택된 날개벽 치수(시작단면 높이, 종점단면 높이, 날개벽 연장) 3D 치수선 및 라벨 생성
// Requirement 1 & 2 & 3 & 4: 선택된 날개벽 치수(시작단면 높이, 종점단면 높이, 날개벽 수평연장) 흰색 3D 치수선 및 간소화 라벨 표출
showWingwallDimensions(wingwallId) {
this.clearDimensionAnnotations();
if (!wingwallId || wingwallId === 'CULVERT') return;
@@ -770,13 +770,6 @@ class Viewer3D {
? (objects.ww_end_left || objects.ww_left)
: (objects.ww_end_right || objects.ww_right);
const dim = wwData?.dimensions || {};
const lengthVal = dim.length ?? (isLeft ? 4.35 : 5.40);
const hStartVal = dim.total_height_start ?? 3.70;
const hEndVal = dim.total_height_end ?? 1.50;
const colorHex = isLeft ? 0x38bdf8 : 0xfbbf24;
let pStartBot, pStartTop, pEndBot, pEndTop;
const sec = wwData?.sections_3d || {};
@@ -816,6 +809,12 @@ class Viewer3D {
}
}
// 실제 3D 모델링 기하 좌표로부터 치수값 정밀 산출 (Requirement 5: 임의 고정값 배제)
const hStartVal = Math.abs(pStartTop.z - pStartBot.z);
const hEndVal = Math.abs(pEndTop.z - pEndBot.z);
// 시작단면과 종점단면 사이의 수평거리를 날개벽 연장으로 정의 (Requirement 2)
const lengthVal = Math.hypot(pEndTop.x - pStartTop.x, pEndTop.y - pStartTop.y);
// 벽체 연장 축 (XY 평면)
const vWall = new THREE.Vector3(pEndTop.x - pStartTop.x, pEndTop.y - pStartTop.y, 0);
const uWall = vWall.clone().normalize();
@@ -825,45 +824,101 @@ class Viewer3D {
? new THREE.Vector3(-uWall.y, uWall.x, 0).normalize()
: new THREE.Vector3(uWall.y, -uWall.x, 0).normalize();
// 1. 시작단면 높이 치수선 및 라벨 (H = 3.70m)
// 1. 시작단면 높이 치수선 (외측 법선 방향 배치, H=00.0m) - Requirement 3 & 4
this.createDimension3D(
pStartBot,
pStartTop,
nWall,
0.55,
`시작단면 H = ${hStartVal.toFixed(2)}m`,
colorHex
0.60,
`H=${hStartVal.toFixed(2)}m`
);
// 2. 종점단면 높이 치수선 및 라벨 (H = 1.50m)
// 2. 종점단면 높이 치수선 (종점 축 외측 방향 배치, H=00.0m) - Requirement 3 & 4
this.createDimension3D(
pEndBot,
pEndTop,
uWall,
0.55,
`종점단면 H = ${hEndVal.toFixed(2)}m`,
colorHex
0.60,
`H=${hEndVal.toFixed(2)}m`
);
// 3. 날개벽 연장 치수선 및 라벨 (L = 4.35m / 5.40m)
this.createDimension3D(
// 3. 날개벽 연장 수평 치수선 (경사가 아닌 수평 방향 배치, L=00.0m) - Requirement 2 & 3 & 4
this.createHorizontalDimension3D(
pStartTop,
pEndTop,
new THREE.Vector3(0, 0, 1),
0.65,
`날개벽 연장 L = ${lengthVal.toFixed(2)}m`,
colorHex
nWall,
0.20,
`L=${lengthVal.toFixed(2)}m`
);
}
createDimension3D(p1, p2, offsetDir, offsetDist, text, colorHex) {
// Requirement 2: 날개벽 연장 수평 치수선 (동일 Z 높이 수평 배치로 경사/기울어짐 방지)
createHorizontalDimension3D(p1, p2, normalDir, normalDist, text) {
const zDim = Math.max(p1.z, p2.z) + 0.60;
const offH = (normalDir && normalDist)
? normalDir.clone().normalize().multiplyScalar(normalDist)
: new THREE.Vector3(0, 0, 0);
const startBase = new THREE.Vector3(p1.x + offH.x, p1.y + offH.y, p1.z);
const endBase = new THREE.Vector3(p2.x + offH.x, p2.y + offH.y, p2.z);
// 치수선의 양 끝단 Z 좌표를 zDim으로 일치시켜 완벽한 수평 보장
const d1 = new THREE.Vector3(startBase.x, startBase.y, zDim);
const d2 = new THREE.Vector3(endBase.x, endBase.y, zDim);
const linePoints = [];
// 보조선 (Extension Lines): 각 단면 상단 기준점에서 수평 치수선 높이까지 수직 연장
const overshoot = 0.15;
linePoints.push(startBase.clone(), new THREE.Vector3(d1.x, d1.y, zDim + overshoot));
linePoints.push(endBase.clone(), new THREE.Vector3(d2.x, d2.y, zDim + overshoot));
// 치수선 (Horizontal Dimension Line)
linePoints.push(d1.clone(), d2.clone());
// 끝단 틱 마커 (End Ticks): 수평 치수선에 직교하는 수직 틱
const tickLen = 0.14;
linePoints.push(
new THREE.Vector3(d1.x, d1.y, zDim - tickLen),
new THREE.Vector3(d1.x, d1.y, zDim + tickLen)
);
linePoints.push(
new THREE.Vector3(d2.x, d2.y, zDim - tickLen),
new THREE.Vector3(d2.x, d2.y, zDim + tickLen)
);
const geom = new THREE.BufferGeometry().setFromPoints(linePoints);
const mat = new THREE.LineBasicMaterial({
color: 0xffffff, // Requirement 4: 모든 치수선 흰색
linewidth: 2,
depthTest: false,
transparent: true,
opacity: 0.95
});
const lineSegs = new THREE.LineSegments(geom, mat);
lineSegs.renderOrder = 999;
this.dimensionGroup.add(lineSegs);
// 치수값 텍스트 라벨 (수평 치수선 상단 중앙 배치)
const mid = new THREE.Vector3().addVectors(d1, d2).multiplyScalar(0.5);
const spritePos = new THREE.Vector3(mid.x, mid.y, zDim + 0.28);
const sprite = this.createDimensionTextSprite(text);
sprite.position.copy(spritePos);
sprite.renderOrder = 1000;
this.scene.add(sprite);
this.dimensionSprites.push(sprite);
}
// 높이 측정용 3D 치수선 생성 (Requirement 3 & 4: 흰색 치수선 및 간소화 표기)
createDimension3D(p1, p2, offsetDir, offsetDist, text) {
const off = offsetDir.clone().normalize().multiplyScalar(offsetDist);
const d1 = p1.clone().add(off);
const d2 = p2.clone().add(off);
const linePoints = [];
// 보조선 (Extension Lines from geometry to dimension line)
// 보조선 (Extension Lines)
const extOver = offsetDir.clone().normalize().multiplyScalar(0.15);
linePoints.push(p1.clone(), d1.clone().add(extOver));
linePoints.push(p2.clone(), d2.clone().add(extOver));
@@ -871,7 +926,7 @@ class Viewer3D {
// 치수선 (Main Dimension Line)
linePoints.push(d1.clone(), d2.clone());
// 끝단 틱 표시 (End Ticks: 시작점과 끝점 및 측정 방향 표시)
// 끝단 틱 표시 (End Ticks)
const dimDir = new THREE.Vector3().subVectors(d2, d1).normalize();
let tickDir = new THREE.Vector3().crossVectors(dimDir, offsetDir).normalize();
if (tickDir.lengthSq() < 0.01) {
@@ -883,7 +938,7 @@ class Viewer3D {
const geom = new THREE.BufferGeometry().setFromPoints(linePoints);
const mat = new THREE.LineBasicMaterial({
color: colorHex,
color: 0xffffff, // Requirement 4: 모든 치수선 흰색
linewidth: 2,
depthTest: false,
transparent: true,
@@ -893,39 +948,41 @@ class Viewer3D {
lineSegs.renderOrder = 999;
this.dimensionGroup.add(lineSegs);
// 치수값 텍스트 라벨 (중앙에 가독성 높게 배치)
// 치수값 텍스트 라벨 (치수선 외측 중앙에 명확히 대응되도록 배치)
const mid = new THREE.Vector3().addVectors(d1, d2).multiplyScalar(0.5);
const spritePos = mid.clone().addScaledVector(offsetDir.clone().normalize(), 0.35);
const spritePos = mid.clone().addScaledVector(offsetDir.clone().normalize(), 0.32);
const sprite = this.createDimensionTextSprite(text, colorHex);
const sprite = this.createDimensionTextSprite(text);
sprite.position.copy(spritePos);
sprite.renderOrder = 1000;
this.scene.add(sprite);
this.dimensionSprites.push(sprite);
}
createDimensionTextSprite(text, colorHex) {
// Requirement 3 & 4: 사각형 테이블(배경 박스 및 테두리) 삭제, 순수 흰색 치수값 텍스트 생성
createDimensionTextSprite(text) {
const canvas = document.createElement('canvas');
canvas.width = 340;
canvas.height = 68;
canvas.width = 256;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// 반투명 다크 배경 필
ctx.fillStyle = 'rgba(15, 23, 42, 0.94)';
ctx.roundRect ? ctx.roundRect(4, 4, 332, 60, 8) : ctx.fillRect(4, 4, 332, 60);
ctx.fill();
// 투명 배경 (사각형 박스 및 테두리 완전 삭제)
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 부재 강조 색상 테두리
ctx.strokeStyle = '#' + new THREE.Color(colorHex).getHexString();
ctx.lineWidth = 2.5;
ctx.stroke();
// 선명한 화이트 볼드 텍스트
ctx.font = 'bold 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px "Segoe UI", Roboto, -apple-system, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 170, 34);
// 3D 뷰포트에서 배경/모델 색상과 무관하게 글자가 또렷이 식별되도록 글자 자체에 미세한 다크 외곽선 적용
ctx.strokeStyle = 'rgba(15, 23, 42, 0.90)';
ctx.lineWidth = 3.5;
ctx.lineJoin = 'round';
ctx.miterLimit = 2;
ctx.strokeText(text, 128, 32);
// 치수값 흰색 표시
ctx.fillStyle = '#ffffff';
ctx.fillText(text, 128, 32);
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
@@ -935,8 +992,8 @@ class Viewer3D {
transparent: true
});
const sprite = new THREE.Sprite(spriteMat);
sprite.scale.set(3.4, 0.68, 1);
sprite.userData.aspect = 340 / 68;
sprite.scale.set(2.8, 0.7, 1);
sprite.userData.aspect = 256 / 64; // 4.0
return sprite;
}