feat(modeling): 2단계 날개벽 선택 시 강조/흐림 대비 및 주요 치수(시작고, 종점고, 연장) 3D 치수선 표출
This commit is contained in:
1 parent
fe6b09f9b2
commit
90a178a7e5
1 file changed
+275
-12
+275
-12
@@ -10,6 +10,8 @@ class Viewer3D {
|
||||
this.labelSprites = [];
|
||||
this.rebarLabelSprites = [];
|
||||
this.wingwallNameSprites = [];
|
||||
this.dimensionGroup = null;
|
||||
this.dimensionSprites = [];
|
||||
this.displayMode = 'solid'; // 'solid', 'wireframe', 'transparent'
|
||||
this.stepMode = 2; // 2: Exterior only, 3: Exterior (ghosted) + 3D Rebars
|
||||
this.selectedObjectId = null;
|
||||
@@ -79,6 +81,11 @@ class Viewer3D {
|
||||
this.rebarGroupRoot = new THREE.Group();
|
||||
this.scene.add(this.rebarGroupRoot);
|
||||
|
||||
// Dimension group for Step 2 selected wingwall measurements
|
||||
this.dimensionGroup = new THREE.Group();
|
||||
this.scene.add(this.dimensionGroup);
|
||||
this.dimensionSprites = [];
|
||||
|
||||
// Ground Grid & Axes
|
||||
this.setupGridAndAxes();
|
||||
|
||||
@@ -115,6 +122,7 @@ class Viewer3D {
|
||||
this.meshes = {};
|
||||
this.clearSectionMarkers();
|
||||
this.clearWingwallNameSprites();
|
||||
this.clearDimensionAnnotations();
|
||||
|
||||
const objects = modelData.objects || {};
|
||||
const botEl = objects.culvert?.dimensions?.bot_el ?? 33.404;
|
||||
@@ -325,6 +333,9 @@ class Viewer3D {
|
||||
|
||||
setStepMode(stepNumber) {
|
||||
this.stepMode = stepNumber;
|
||||
if (stepNumber === 3) {
|
||||
this.clearDimensionAnnotations();
|
||||
}
|
||||
this.applyStepVisualState();
|
||||
}
|
||||
|
||||
@@ -644,7 +655,7 @@ class Viewer3D {
|
||||
const center = new THREE.Vector3();
|
||||
box.getCenter(center);
|
||||
const sprite = this.createTextSprite(item.name, item.color);
|
||||
sprite.position.set(center.x, center.y, box.max.z + 0.65);
|
||||
sprite.position.set(center.x, center.y, box.max.z + 1.25);
|
||||
sprite.userData.wingwallId = item.id;
|
||||
sprite.visible = false;
|
||||
this.scene.add(sprite);
|
||||
@@ -680,18 +691,270 @@ class Viewer3D {
|
||||
selectObject(objectId) {
|
||||
this.selectedObjectId = objectId;
|
||||
if (this.stepMode === 3) return;
|
||||
for (const id in this.meshes) {
|
||||
const m = this.meshes[id];
|
||||
const mat = m.group.userData.mat;
|
||||
if (id === objectId) {
|
||||
mat.emissive = new THREE.Color(0x2563eb);
|
||||
mat.emissiveIntensity = 0.45;
|
||||
} else {
|
||||
mat.emissive = new THREE.Color(0x000000);
|
||||
|
||||
const isCulvert = (!objectId || objectId === 'CULVERT');
|
||||
|
||||
if (isCulvert) {
|
||||
// 2.1 암거 본체 선택: 메인 화면에 암거 본체 및 모든 구조물을 정상 진하게 표시
|
||||
this.clearDimensionAnnotations();
|
||||
for (const id in this.meshes) {
|
||||
const m = this.meshes[id];
|
||||
const mat = m.group.userData.mat;
|
||||
const edgesMat = m.group.userData.edgesMat;
|
||||
|
||||
mat.transparent = false;
|
||||
mat.opacity = 1.0;
|
||||
mat.depthWrite = true;
|
||||
mat.color.setHex(m.baseColor);
|
||||
mat.emissive.setHex(0x000000);
|
||||
mat.emissiveIntensity = 0;
|
||||
|
||||
if (edgesMat) {
|
||||
edgesMat.transparent = true;
|
||||
edgesMat.opacity = 0.35;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 2.2 날개벽 선택: 선택된 날개벽은 진하게, 나머지 구조물은 흐리게 표시
|
||||
this.clearDimensionAnnotations();
|
||||
for (const id in this.meshes) {
|
||||
const m = this.meshes[id];
|
||||
const mat = m.group.userData.mat;
|
||||
const edgesMat = m.group.userData.edgesMat;
|
||||
|
||||
if (id === objectId) {
|
||||
// 선택된 날개벽: 진하게 선명하게 강조
|
||||
mat.transparent = false;
|
||||
mat.opacity = 1.0;
|
||||
mat.depthWrite = true;
|
||||
mat.color.setHex(m.baseColor);
|
||||
mat.emissive.setHex((id === 'WW_END_LEFT' || id === 'ww_left') ? 0x0284c7 : 0xd97706);
|
||||
mat.emissiveIntensity = 0.30;
|
||||
|
||||
if (edgesMat) {
|
||||
edgesMat.transparent = false;
|
||||
edgesMat.opacity = 0.85;
|
||||
}
|
||||
} else {
|
||||
// 나머지 구조물: 흐리게 (위치와 형상은 파악 가능한 반투명)
|
||||
mat.transparent = true;
|
||||
mat.opacity = 0.18;
|
||||
mat.depthWrite = false;
|
||||
mat.color.setHex(0x64748b);
|
||||
mat.emissive.setHex(0x000000);
|
||||
mat.emissiveIntensity = 0;
|
||||
|
||||
if (edgesMat) {
|
||||
edgesMat.transparent = true;
|
||||
edgesMat.opacity = 0.18;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 선택된 날개벽의 주요 치수(시작단면 높이, 종점단면 높이, 날개벽 연장) 표시
|
||||
this.showWingwallDimensions(objectId);
|
||||
}
|
||||
|
||||
this.updateWingwallNameSpritesVisibility();
|
||||
}
|
||||
|
||||
// Requirement 3: 선택된 날개벽 치수(시작단면 높이, 종점단면 높이, 날개벽 연장) 3D 치수선 및 라벨 생성
|
||||
showWingwallDimensions(wingwallId) {
|
||||
this.clearDimensionAnnotations();
|
||||
if (!wingwallId || wingwallId === 'CULVERT') return;
|
||||
|
||||
const objects = this.modelData?.objects || {};
|
||||
const isLeft = (wingwallId === 'WW_END_LEFT' || wingwallId === 'ww_left' || wingwallId === 'ww_end_left');
|
||||
|
||||
const wwData = isLeft
|
||||
? (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 || {};
|
||||
|
||||
if (isLeft) {
|
||||
const secA = sec['A-A'];
|
||||
const secB = sec['B-B'];
|
||||
if (secA && secA.length >= 6) {
|
||||
pStartBot = new THREE.Vector3(secA[5][0], secA[5][1], secA[0][2]);
|
||||
pStartTop = new THREE.Vector3(secA[5][0], secA[5][1], secA[4][2]);
|
||||
} else {
|
||||
pStartBot = new THREE.Vector3(15.731, 42.650, 33.404);
|
||||
pStartTop = new THREE.Vector3(15.731, 42.650, 37.104);
|
||||
}
|
||||
if (secB && secB.length >= 6) {
|
||||
pEndBot = new THREE.Vector3(secB[5][0], secB[5][1], secB[0][2]);
|
||||
pEndTop = new THREE.Vector3(secB[5][0], secB[5][1], secB[4][2]);
|
||||
} else {
|
||||
pEndBot = new THREE.Vector3(18.759, 45.773, 33.523);
|
||||
pEndTop = new THREE.Vector3(18.759, 45.773, 35.023);
|
||||
}
|
||||
} else {
|
||||
const secC = sec['C-C'];
|
||||
const secD = sec['D-D'];
|
||||
if (secC && secC.length >= 6) {
|
||||
pStartBot = new THREE.Vector3(secC[5][0], secC[5][1], secC[0][2]);
|
||||
pStartTop = new THREE.Vector3(secC[5][0], secC[5][1], secC[4][2]);
|
||||
} else {
|
||||
pStartBot = new THREE.Vector3(16.287, 40.650, 33.404);
|
||||
pStartTop = new THREE.Vector3(16.287, 40.650, 37.104);
|
||||
}
|
||||
if (secD && secD.length >= 6) {
|
||||
pEndBot = new THREE.Vector3(secD[5][0], secD[5][1], secD[0][2]);
|
||||
pEndTop = new THREE.Vector3(secD[5][0], secD[5][1], secD[4][2]);
|
||||
} else {
|
||||
pEndBot = new THREE.Vector3(21.004, 38.022, 33.523);
|
||||
pEndTop = new THREE.Vector3(21.004, 38.022, 35.023);
|
||||
}
|
||||
}
|
||||
this.updateWingwallNameSpritesVisibility();
|
||||
|
||||
// 벽체 연장 축 (XY 평면)
|
||||
const vWall = new THREE.Vector3(pEndTop.x - pStartTop.x, pEndTop.y - pStartTop.y, 0);
|
||||
const uWall = vWall.clone().normalize();
|
||||
|
||||
// 벽체 외측 법선 벡터 (XY 평면)
|
||||
const nWall = isLeft
|
||||
? new THREE.Vector3(-uWall.y, uWall.x, 0).normalize()
|
||||
: new THREE.Vector3(uWall.y, -uWall.x, 0).normalize();
|
||||
|
||||
// 1. 시작단면 높이 치수선 및 라벨 (H = 3.70m)
|
||||
this.createDimension3D(
|
||||
pStartBot,
|
||||
pStartTop,
|
||||
nWall,
|
||||
0.55,
|
||||
`시작단면 H = ${hStartVal.toFixed(2)}m`,
|
||||
colorHex
|
||||
);
|
||||
|
||||
// 2. 종점단면 높이 치수선 및 라벨 (H = 1.50m)
|
||||
this.createDimension3D(
|
||||
pEndBot,
|
||||
pEndTop,
|
||||
uWall,
|
||||
0.55,
|
||||
`종점단면 H = ${hEndVal.toFixed(2)}m`,
|
||||
colorHex
|
||||
);
|
||||
|
||||
// 3. 날개벽 연장 치수선 및 라벨 (L = 4.35m / 5.40m)
|
||||
this.createDimension3D(
|
||||
pStartTop,
|
||||
pEndTop,
|
||||
new THREE.Vector3(0, 0, 1),
|
||||
0.65,
|
||||
`날개벽 연장 L = ${lengthVal.toFixed(2)}m`,
|
||||
colorHex
|
||||
);
|
||||
}
|
||||
|
||||
createDimension3D(p1, p2, offsetDir, offsetDist, text, colorHex) {
|
||||
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)
|
||||
const extOver = offsetDir.clone().normalize().multiplyScalar(0.15);
|
||||
linePoints.push(p1.clone(), d1.clone().add(extOver));
|
||||
linePoints.push(p2.clone(), d2.clone().add(extOver));
|
||||
|
||||
// 치수선 (Main Dimension Line)
|
||||
linePoints.push(d1.clone(), d2.clone());
|
||||
|
||||
// 끝단 틱 표시 (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) {
|
||||
tickDir = new THREE.Vector3(0, 0, 1);
|
||||
}
|
||||
const tickLen = 0.14;
|
||||
linePoints.push(d1.clone().addScaledVector(tickDir, tickLen), d1.clone().addScaledVector(tickDir, -tickLen));
|
||||
linePoints.push(d2.clone().addScaledVector(tickDir, tickLen), d2.clone().addScaledVector(tickDir, -tickLen));
|
||||
|
||||
const geom = new THREE.BufferGeometry().setFromPoints(linePoints);
|
||||
const mat = new THREE.LineBasicMaterial({
|
||||
color: colorHex,
|
||||
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 = mid.clone().addScaledVector(offsetDir.clone().normalize(), 0.35);
|
||||
|
||||
const sprite = this.createDimensionTextSprite(text, colorHex);
|
||||
sprite.position.copy(spritePos);
|
||||
sprite.renderOrder = 1000;
|
||||
this.scene.add(sprite);
|
||||
this.dimensionSprites.push(sprite);
|
||||
}
|
||||
|
||||
createDimensionTextSprite(text, colorHex) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 340;
|
||||
canvas.height = 68;
|
||||
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.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.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(text, 170, 34);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
const spriteMat = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
depthTest: false,
|
||||
transparent: true
|
||||
});
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.scale.set(3.4, 0.68, 1);
|
||||
sprite.userData.aspect = 340 / 68;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
clearDimensionAnnotations() {
|
||||
if (this.dimensionGroup) {
|
||||
while (this.dimensionGroup.children.length > 0) {
|
||||
const obj = this.dimensionGroup.children[0];
|
||||
if (obj.geometry) obj.geometry.dispose();
|
||||
if (obj.material) obj.material.dispose();
|
||||
this.dimensionGroup.remove(obj);
|
||||
}
|
||||
}
|
||||
for (const sp of this.dimensionSprites) {
|
||||
this.scene.remove(sp);
|
||||
if (sp.material?.map) sp.material.map.dispose();
|
||||
if (sp.material) sp.material.dispose();
|
||||
}
|
||||
this.dimensionSprites = [];
|
||||
}
|
||||
|
||||
toggleObjectVisibility(objectId, visible) {
|
||||
@@ -837,8 +1100,8 @@ class Viewer3D {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
this.controls.update();
|
||||
|
||||
// Requirement 3.3: 줌 확대/축소 시에도 3D 텍스트 크기 일정 유지 (Screen-space constant size)
|
||||
const sprites = [...this.rebarLabelSprites, ...this.labelSprites, ...this.wingwallNameSprites];
|
||||
// Requirement 2.3 & 3.3: 줌 확대/축소 시에도 3D 텍스트 크기 일정 유지 (Screen-space constant size)
|
||||
const sprites = [...this.rebarLabelSprites, ...this.labelSprites, ...this.wingwallNameSprites, ...this.dimensionSprites];
|
||||
for (const sp of sprites) {
|
||||
if (sp && sp.visible) {
|
||||
const dist = this.camera.position.distanceTo(sp.position);
|
||||
|
||||
Reference in new issue
Block a user