Files
dxf-to-3d-gasiseol/out/viewer-correct.html
T
Claude Haiku d2ad629213 부재 컨트롤 패널 추가 - 온오프 및 줌핏 기능
기능 추가:

1. 패널에 부재 구성 표시
   ✓ 부재명 (한글)
   ✓ 규격 정보 (mm 단위)
   ✓ 부재 개수
   ✓ 색상 표시

2. 부재 온오프 제어
   ✓ 부재 타입별 체크박스
   ✓ 체크 해제하면 해당 부재 숨김
   ✓ 실시간 표시/숨김

3. 줌핏 기능
   ✓ "🎯 줌핏" 버튼 추가
   ✓ 활성화된 부재만 경계 계산
   ✓ 자동 카메라 조정
   ✓ 선택된 부재들이 화면 중앙에 표시

부재 그룹 관리:
- memberGroups 객체로 부재 타입별 메시 추적
- 메시 userData에 groupName 저장
- 체크박스와 메시 가시성 연동

사용법:
1. 패널의 체크박스로 부재 표시/숨김
2. "줌핏" 버튼으로 선택된 부재에 자동 줌
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-19 17:37:05 +09:00

586 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>가시설 3D 모델 뷰어</title>
<style>
html, body {
margin: 0;
padding: 0;
border: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0f1419;
display: flex;
color: #fff;
}
.container {
width: 100%;
height: 100%;
display: flex;
overflow: hidden;
}
#canvas {
flex: 1;
background: linear-gradient(135deg, #1a3a52 0%, #0d1f2d 100%);
overflow: hidden;
}
.panel {
width: fit-content;
min-width: 300px;
max-width: 650px;
height: 100%;
background: #1a1f2e;
padding: 12px;
overflow-y: auto;
overflow-x: hidden;
border-left: 1px solid #2d3748;
display: flex;
flex-direction: column;
font-size: 12px;
}
.panel::-webkit-scrollbar {
width: 6px;
}
.panel::-webkit-scrollbar-track {
background: #1a1f2e;
}
.panel::-webkit-scrollbar-thumb {
background: #4a5568;
border-radius: 3px;
}
.panel::-webkit-scrollbar-thumb:hover {
background: #718096;
}
.panel h2 {
color: #10b981;
font-size: 13px;
margin-bottom: 6px;
padding-bottom: 4px;
border-bottom: 2px solid #10b981;
font-weight: 700;
}
.panel h3 {
color: #cbd5e0;
font-size: 10px;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.badge {
display: inline-block;
background: #10b981;
color: #1a1f2e;
padding: 3px 8px;
border-radius: 14px;
font-size: 9px;
font-weight: 700;
margin-bottom: 6px;
width: fit-content;
}
.loading {
padding: 8px;
background: #252d3d;
border-radius: 6px;
color: #fbbf24;
font-size: 11px;
margin-bottom: 6px;
}
.section {
margin-bottom: 8px;
padding: 8px;
background: #252d3d;
border-radius: 6px;
border-left: 3px solid #10b981;
}
.stat-row {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #cbd5e0;
margin: 3px 0;
}
.stat-label {
color: #a0aec0;
}
.stat-value {
font-weight: 600;
color: #10b981;
}
.legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
}
.legend-color {
width: 10px;
height: 10px;
border-radius: 2px;
}
.controls {
padding: 8px;
background: #252d3d;
border-radius: 6px;
margin-top: auto;
font-size: 10px;
color: #cbd5e0;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<canvas id="canvas"></canvas>
<div class="panel">
<h2>📊 모델 정보</h2>
<span class="badge">가시설 계획도 3D</span>
<div id="loadingMsg" class="loading">
⏳ 모델 로드 중...
</div>
<div class="section">
<h3>📈 부재 수량</h3>
<div class="stat-row">
<span class="stat-label">총 부재:</span>
<span class="stat-value" id="totalParts">-</span>
</div>
<div class="stat-row">
<span class="stat-label">총 부피:</span>
<span class="stat-value" id="totalVolume">-</span>
</div>
</div>
<div class="section">
<h3>🏗️ 부재 구성</h3>
<div style="font-size: 9px; line-height: 1.8; color: #cbd5e0;">
<label style="display: flex; align-items: center; margin-bottom: 4px; cursor: pointer;">
<input type="checkbox" id="cb-gasiseol4" checked style="margin-right: 6px; cursor: pointer;">
<span style="color: #ff6b6b;">■</span>
<span style="margin-left: 4px;"><strong>가시설4</strong> (H-400×400)</span>
<span style="margin-left: auto; color: #10b981;">52개</span>
</label>
<label style="display: flex; align-items: center; margin-bottom: 4px; cursor: pointer;">
<input type="checkbox" id="cb-hpile2" checked style="margin-right: 6px; cursor: pointer;">
<span style="color: #7ed321;">■</span>
<span style="margin-left: 4px;"><strong>h-pile2</strong> (φ1000×8000)</span>
<span style="margin-left: auto; color: #10b981;">244개</span>
</label>
<label style="display: flex; align-items: center; margin-bottom: 4px; cursor: pointer;">
<input type="checkbox" id="cb-tolyupan" checked style="margin-right: 6px; cursor: pointer;">
<span style="color: #4a90e2;">■</span>
<span style="margin-left: 4px;"><strong>토류판</strong> (PL-700×360×14)</span>
<span style="margin-left: auto; color: #10b981;">240개</span>
</label>
<label style="display: flex; align-items: center; margin-bottom: 4px; cursor: pointer;">
<input type="checkbox" id="cb-hpile" checked style="margin-right: 6px; cursor: pointer;">
<span style="color: #7ed321;">■</span>
<span style="margin-left: 4px;"><strong>H-PILE</strong> (H-400×400)</span>
<span style="margin-left: auto; color: #10b981;">26개</span>
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="cb-strut" checked style="margin-right: 6px; cursor: pointer;">
<span style="color: #f5a623;">■</span>
<span style="margin-left: 4px;"><strong>스터럿</strong> (200×200×4000)</span>
<span style="margin-left: auto; color: #10b981;">26개</span>
</label>
</div>
<button id="btnZoomFit" style="width: 100%; margin-top: 8px; padding: 6px; background: #10b981; color: #1a1f2e; border: none; border-radius: 4px; font-size: 10px; font-weight: 700; cursor: pointer;">🎯 줌핏</button>
</div>
<div class="section">
<h3>🎨 범례</h3>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #ff6b6b;"></div>
<span>H-빔</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #7ed321;"></div>
<span>파일</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4a90e2;"></div>
<span>판재</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f5a623;"></div>
<span>버팀목</span>
</div>
</div>
</div>
<div class="controls">
<h3>⌨️ 조작</h3>
좌클릭 드래그: 회전<br>
우클릭 드래그: 이동<br>
휠: 확대/축소
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const canvas = document.getElementById('canvas');
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a3a52);
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 500000);
camera.position.set(-216, -2554, 3000);
camera.lookAt(-216, -2554, 0);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
// 조명
const directional = new THREE.DirectionalLight(0xffffff, 1.0);
directional.position.set(100000, 100000, 100000);
directional.castShadow = true;
scene.add(directional);
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const hemis = new THREE.HemisphereLight(0x87ceeb, 0x333333, 0.4);
scene.add(hemis);
// 추가 조명
const pointLight = new THREE.PointLight(0xffffff, 0.8, 10000);
pointLight.position.set(500, 500, 500);
scene.add(pointLight);
console.log('=== 3D 뷰어 초기화 ===');
// 간단한 마우스 제어
let isDragging = false;
let mouseX = 0, mouseY = 0;
let cameraRotX = 0, cameraRotY = 0;
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
mouseX = e.clientX;
mouseY = e.clientY;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - mouseX;
const deltaY = e.clientY - mouseY;
cameraRotY += deltaX * 0.005;
cameraRotX += deltaY * 0.005;
const distance = 3000;
camera.position.x = -216 + Math.sin(cameraRotY) * Math.cos(cameraRotX) * distance;
camera.position.y = -2554 + Math.sin(cameraRotX) * distance;
camera.position.z = Math.cos(cameraRotY) * Math.cos(cameraRotX) * distance;
camera.lookAt(-216, -2554, 0);
mouseX = e.clientX;
mouseY = e.clientY;
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const distance = Math.sqrt(
Math.pow(camera.position.x + 216, 2) +
Math.pow(camera.position.y + 2554, 2) +
Math.pow(camera.position.z, 2)
);
const newDistance = Math.max(500, distance + e.deltaY * 5);
const direction = new THREE.Vector3(
camera.position.x + 216,
camera.position.y + 2554,
camera.position.z
).normalize();
camera.position.set(
-216 + direction.x * newDistance,
-2554 + direction.y * newDistance,
direction.z * newDistance
);
camera.lookAt(-216, -2554, 0);
});
// 재질
const getMaterial = (type) => {
const colors = {
'H-Beam': 0xff6b6b,
'Pile': 0x7ed321,
'Panel': 0x4a90e2,
'Strut': 0xf5a623
};
const color = colors[type] || 0x999999;
return new THREE.MeshStandardMaterial({ color, roughness: 0.7, metalness: 0.2 });
};
const DISPLAY_SCALE = 0.01;
// 부재 그룹 추적 (온오프 제어용)
const memberGroups = {
'가시설4': [],
'h-pile2': [],
'토류판': [],
'H-PILE': [],
'스터럿': []
};
// 샘플 부재 (테스트용)
const testGeo = new THREE.BoxGeometry(4, 4, 80);
const testMat = new THREE.MeshStandardMaterial({ color: 0x7ed321, roughness: 0.7, emissive: 0x2d5a2d });
const testMesh = new THREE.Mesh(testGeo, testMat);
testMesh.position.set(0, -2554, 100);
testMesh.userData.groupName = 'h-pile2';
scene.add(testMesh);
memberGroups['h-pile2'].push(testMesh);
console.log('✓ 샘플 부재 추가됨');
// 그리드 & 축
const gridHelper = new THREE.GridHelper(3000, 20, 0x444444, 0x222222);
gridHelper.position.y = -2300;
scene.add(gridHelper);
const axisHelper = new THREE.AxesHelper(500);
scene.add(axisHelper);
// stats.json 로드
console.log('stats.json 로드 시작...');
fetch('gasiseol-correct-v2-stats.json')
.then(r => {
console.log('응답:', r.status);
return r.json();
})
.then(data => {
console.log('✓ JSON 파싱 완료');
console.log('총 부재:', data.totalParts);
let count = 0;
const SCALE = 0.01; // mm → cm
data.parts.forEach(part => {
let geometry;
const { blockType, type, spec, position, volume } = part;
// 규격에 맞는 정확한 형상 생성
if (type === 'Pile') {
// H-PILE: H-400×400×8000
const w = 400 * SCALE;
const h = 400 * SCALE;
const d = 8000 * SCALE;
geometry = new THREE.BoxGeometry(w, h, d);
} else if (type === 'Panel') {
// 판재: PL-700×360×14
let w = 700, h = 360, th = 14;
if (spec && spec.includes('×')) {
const m = spec.match(/PL\.-(\d+)×(\d+)×(\d+)/);
if (m) {
w = parseInt(m[1]);
h = parseInt(m[2]);
th = parseInt(m[3]);
}
}
geometry = new THREE.BoxGeometry(
w * SCALE,
h * SCALE,
th * SCALE
);
} else if (type === 'H-Beam') {
// H-빔: H-400×400×13×21 (깊이는 6000 추정)
let w = 400, h = 400, d = 6000;
if (spec && spec.includes('H-')) {
const m = spec.match(/H-(\d+)×(\d+)/);
if (m) {
w = parseInt(m[1]);
h = parseInt(m[2]);
}
}
geometry = new THREE.BoxGeometry(
w * SCALE,
h * SCALE,
d * SCALE
);
} else if (type === 'Strut') {
// 버팀목: 200×200×4000
geometry = new THREE.BoxGeometry(
200 * SCALE,
200 * SCALE,
4000 * SCALE
);
} else {
geometry = new THREE.BoxGeometry(300 * SCALE, 300 * SCALE, 3000 * SCALE);
}
const mesh = new THREE.Mesh(geometry, getMaterial(type));
mesh.position.set(position.x, position.y, position.z + 10);
mesh.userData = { blockType, type, spec, volume, groupName: blockType };
scene.add(mesh);
// 부재 그룹에 추가
if (memberGroups[blockType]) {
memberGroups[blockType].push(mesh);
}
count++;
if (count <= 3 || count % 100 === 0) {
console.log(`[${count}] ${blockType} (${spec}) @ (${position.x.toFixed(0)}, ${position.y.toFixed(0)})`);
}
});
console.log('✓ ' + count + '개 부재 추가됨');
document.getElementById('totalParts').textContent = data.totalParts + '개';
document.getElementById('totalVolume').textContent = Math.round(data.totalVolume).toLocaleString() + ' m³';
document.getElementById('loadingMsg').style.display = 'none';
console.log('✅ 완료!');
})
.catch(err => {
console.error('❌ 오류:', err);
document.getElementById('loadingMsg').innerHTML = '❌ 로드 실패<br>' + err.message;
});
// 부재 토글 기능
const checkboxes = {
'가시설4': document.getElementById('cb-gasiseol4'),
'h-pile2': document.getElementById('cb-hpile2'),
'토류판': document.getElementById('cb-tolyupan'),
'H-PILE': document.getElementById('cb-hpile'),
'스터럿': document.getElementById('cb-strut')
};
Object.entries(checkboxes).forEach(([name, checkbox]) => {
checkbox.addEventListener('change', (e) => {
const isVisible = e.target.checked;
memberGroups[name].forEach(mesh => {
mesh.visible = isVisible;
});
console.log(`${name}: ${isVisible ? '표시' : '숨김'}`);
});
});
// 줌핏 기능
document.getElementById('btnZoomFit').addEventListener('click', () => {
// 활성화된 부재들의 경계 계산
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
let minZ = Infinity, maxZ = -Infinity;
let hasVisible = false;
Object.entries(memberGroups).forEach(([name, meshes]) => {
if (checkboxes[name].checked) {
meshes.forEach(mesh => {
if (mesh.geometry.boundingBox === null) {
mesh.geometry.computeBoundingBox();
}
const box = mesh.geometry.boundingBox;
const pos = mesh.position;
minX = Math.min(minX, pos.x + box.min.x);
maxX = Math.max(maxX, pos.x + box.max.x);
minY = Math.min(minY, pos.y + box.min.y);
maxY = Math.max(maxY, pos.y + box.max.y);
minZ = Math.min(minZ, pos.z + box.min.z);
maxZ = Math.max(maxZ, pos.z + box.max.z);
hasVisible = true;
});
}
});
if (!hasVisible) {
console.log('표시된 부재가 없습니다');
return;
}
// 경계의 중심과 크기 계산
const center = new THREE.Vector3(
(minX + maxX) / 2,
(minY + maxY) / 2,
(minZ + maxZ) / 2
);
const size = new THREE.Vector3(
maxX - minX,
maxY - minY,
maxZ - minZ
);
const maxDim = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
camera.position.set(center.x, center.y, center.z + cameraZ * 1.5);
camera.lookAt(center);
console.log('✓ 줌핏 완료');
});
// 렌더링
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
// 리사이징
window.addEventListener('resize', () => {
const newWidth = canvas.clientWidth;
const newHeight = canvas.clientHeight;
camera.aspect = newWidth / newHeight;
camera.updateProjectionMatrix();
renderer.setSize(newWidth, newHeight);
});
</script>
</body>
</html>