개선사항: - 패널 너비 축소 (450→350px) - 명확한 섹션 구분 - 부재 구성 상세 정보 추가 - 조작 가이드 명시화 - 색상 범례 개선 역할: • 📈 부재 수량: 전체 통계 • 🏗️ 부재 구성: 각 타입별 상세 정보 • 🎨 범례: 색상 의미 • ⌨️ 조작: 마우스/화면 사용 방법 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
494 lines
17 KiB
HTML
494 lines
17 KiB
HTML
<!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>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: #0f1419;
|
||
height: 100vh;
|
||
display: flex;
|
||
color: #fff;
|
||
}
|
||
|
||
.container {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
}
|
||
|
||
#canvas {
|
||
flex: 1;
|
||
background: linear-gradient(135deg, #1a3a52 0%, #0d1f2d 100%);
|
||
}
|
||
|
||
.panel {
|
||
width: 350px;
|
||
background: #1a1f2e;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
border-left: 1px solid #2d3748;
|
||
display: flex;
|
||
flex-direction: column;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.panel h2 {
|
||
color: #10b981;
|
||
font-size: 14px;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 2px solid #10b981;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.panel h3 {
|
||
color: #cbd5e0;
|
||
font-size: 11px;
|
||
margin-bottom: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
background: #10b981;
|
||
color: #1a1f2e;
|
||
padding: 4px 10px;
|
||
border-radius: 16px;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
margin-bottom: 12px;
|
||
width: fit-content;
|
||
}
|
||
|
||
.loading {
|
||
padding: 15px;
|
||
background: #252d3d;
|
||
border-radius: 8px;
|
||
color: #fbbf24;
|
||
font-size: 13px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #252d3d;
|
||
border-radius: 8px;
|
||
border-left: 3px solid #10b981;
|
||
}
|
||
|
||
.section h3 {
|
||
color: #10b981;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
color: #cbd5e0;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #a0aec0;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: 600;
|
||
color: #10b981;
|
||
}
|
||
|
||
.info-box {
|
||
padding: 12px;
|
||
background: #1a3a1a;
|
||
border-left: 3px solid #10b981;
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
color: #7fd87f;
|
||
margin-bottom: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.part-type {
|
||
padding: 10px;
|
||
margin: 8px 0;
|
||
background: #1a1f2e;
|
||
border-radius: 6px;
|
||
border-left: 4px solid;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.part-type.h-beam { border-left-color: #ff6b6b; }
|
||
.part-type.pile { border-left-color: #7ed321; }
|
||
.part-type.panel { border-left-color: #4a90e2; }
|
||
.part-type.strut { border-left-color: #f5a623; }
|
||
|
||
.part-name {
|
||
font-weight: 600;
|
||
color: #e2e8f0;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.part-info {
|
||
color: #718096;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.controls {
|
||
padding: 12px;
|
||
background: #252d3d;
|
||
border-radius: 8px;
|
||
margin-top: 15px;
|
||
font-size: 11px;
|
||
color: #cbd5e0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.legend {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.legend-color {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: #1a1f2e;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #4a5568;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #718096;
|
||
}
|
||
</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" style="margin-top: 10px;">
|
||
<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: 11px; line-height: 1.6; color: #cbd5e0;">
|
||
<div style="margin-bottom: 8px;">
|
||
<strong style="color: #ff6b6b;">가시설4</strong> (H-빔)<br>
|
||
<span style="color: #a0aec0;">52개</span>
|
||
</div>
|
||
<div style="margin-bottom: 8px;">
|
||
<strong style="color: #7ed321;">h-pile2</strong> (기초파일)<br>
|
||
<span style="color: #a0aec0;">244개, H-400×400×8000mm</span>
|
||
</div>
|
||
<div style="margin-bottom: 8px;">
|
||
<strong style="color: #4a90e2;">토류판</strong> (흙막이판)<br>
|
||
<span style="color: #a0aec0;">240개, PL-700×360×14mm</span>
|
||
</div>
|
||
<div style="margin-bottom: 8px;">
|
||
<strong style="color: #7ed321;">H-PILE</strong> (파일)<br>
|
||
<span style="color: #a0aec0;">26개</span>
|
||
</div>
|
||
<div>
|
||
<strong style="color: #f5a623;">스터럿</strong> (버팀목)<br>
|
||
<span style="color: #a0aec0;">26개</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 범례 섹션 -->
|
||
<div class="section">
|
||
<h3>🎨 범례</h3>
|
||
<div class="legend" style="grid-template-columns: 1fr 1fr; gap: 8px;">
|
||
<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>
|
||
<div style="font-size: 11px; color: #cbd5e0; line-height: 1.8;">
|
||
<strong>마우스:</strong><br>
|
||
• 좌클릭 드래그 = 회전<br>
|
||
• 휠 스크롤 = 확대/축소<br>
|
||
<br>
|
||
<strong>화면:</strong><br>
|
||
• 3D 배치 확인<br>
|
||
• 부재 형상 검증<br>
|
||
• 구조 파악
|
||
</div>
|
||
</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);
|
||
// 부재 중심을 향하도록 설정 (정확한 좌표 범위)
|
||
// 중심: (-216, -2554), 폭: 1373, 높이: 552
|
||
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 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; // mm → cm (1:100)
|
||
|
||
// stats.json 로드 및 부재 생성
|
||
fetch('gasiseol-correct-v2-stats.json')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
console.log('📊 로드됨:', data.totalParts, '개 부재');
|
||
|
||
let addedCount = 0;
|
||
// 각 부재를 3D 메시로 생성
|
||
data.parts.forEach(part => {
|
||
let geometry;
|
||
const { blockType, type, spec, position, volume } = part;
|
||
|
||
// 부재 타입별 3D 형상 생성
|
||
if (type === 'Pile') {
|
||
// H-PILE 형상 (H-beam 박스)
|
||
geometry = new THREE.BoxGeometry(
|
||
400 * DISPLAY_SCALE,
|
||
400 * DISPLAY_SCALE,
|
||
8000 * DISPLAY_SCALE
|
||
);
|
||
} else if (type === 'Panel') {
|
||
// 판재 - 규격에서 가로×세로×두께 추출
|
||
let w = 700, h = 360, d = 14;
|
||
if (spec && spec.includes('×')) {
|
||
const match = spec.match(/PL\.-(\d+)×(\d+)×(\d+)/);
|
||
if (match) {
|
||
w = parseInt(match[1]);
|
||
h = parseInt(match[2]);
|
||
d = parseInt(match[3]);
|
||
}
|
||
}
|
||
geometry = new THREE.BoxGeometry(
|
||
w * DISPLAY_SCALE,
|
||
h * DISPLAY_SCALE,
|
||
d * DISPLAY_SCALE
|
||
);
|
||
} else if (type === 'H-Beam') {
|
||
// H-빔 - H-400×400×13×21
|
||
let w = 400, h = 400, d = 6000;
|
||
if (spec && spec.includes('H-')) {
|
||
const match = spec.match(/H-(\d+)×(\d+)/);
|
||
if (match) {
|
||
w = parseInt(match[1]);
|
||
h = parseInt(match[2]);
|
||
}
|
||
}
|
||
geometry = new THREE.BoxGeometry(
|
||
w * DISPLAY_SCALE,
|
||
h * DISPLAY_SCALE,
|
||
d * DISPLAY_SCALE
|
||
);
|
||
} else if (type === 'Strut') {
|
||
// 버팀목 - 200×200×4000
|
||
geometry = new THREE.BoxGeometry(
|
||
200 * DISPLAY_SCALE,
|
||
200 * DISPLAY_SCALE,
|
||
4000 * DISPLAY_SCALE
|
||
);
|
||
} else {
|
||
geometry = new THREE.BoxGeometry(
|
||
300 * DISPLAY_SCALE,
|
||
300 * DISPLAY_SCALE,
|
||
3000 * DISPLAY_SCALE
|
||
);
|
||
}
|
||
|
||
const mesh = new THREE.Mesh(geometry, getMaterial(type));
|
||
mesh.position.set(position.x, position.y, position.z);
|
||
mesh.userData = { blockType, type, spec, volume };
|
||
|
||
scene.add(mesh);
|
||
addedCount++;
|
||
});
|
||
|
||
console.log('✅ ' + addedCount + '개 부재 추가됨');
|
||
|
||
// UI 업데이트
|
||
document.getElementById('totalParts').textContent = data.totalParts + '개';
|
||
document.getElementById('totalVolume').textContent = Math.round(data.totalVolume).toLocaleString() + ' m³';
|
||
document.getElementById('loadingMsg').style.display = 'none';
|
||
|
||
console.log('✅ 3D 모델 생성 완료');
|
||
})
|
||
.catch(err => {
|
||
console.error('❌ 로드 오류:', err);
|
||
document.getElementById('loadingMsg').innerHTML = '❌ 부재 데이터 로드 실패<br>' + err.message;
|
||
});
|
||
|
||
// 그리드 & 축
|
||
const gridHelper = new THREE.GridHelper(300000, 20, 0x444444, 0x222222);
|
||
gridHelper.position.y = -1000;
|
||
scene.add(gridHelper);
|
||
|
||
const axisHelper = new THREE.AxesHelper(50000);
|
||
scene.add(axisHelper);
|
||
|
||
// 마우스 조작
|
||
let isDragging = false;
|
||
let previousMousePosition = { x: 0, y: 0 };
|
||
|
||
canvas.addEventListener('mousedown', (e) => {
|
||
isDragging = true;
|
||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
||
});
|
||
|
||
canvas.addEventListener('mousemove', (e) => {
|
||
if (!isDragging) return;
|
||
|
||
const deltaX = e.clientX - previousMousePosition.x;
|
||
const deltaY = e.clientY - previousMousePosition.y;
|
||
|
||
const targetPoint = new THREE.Vector3(0, 0, 0);
|
||
const direction = camera.position.clone().sub(targetPoint);
|
||
|
||
const rotationMatrix = new THREE.Matrix4()
|
||
.makeRotationAxis(new THREE.Vector3(0, 1, 0), deltaX * 0.005);
|
||
|
||
const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
|
||
rotationMatrix.multiply(new THREE.Matrix4().makeRotationAxis(rightVector, deltaY * 0.005));
|
||
|
||
direction.applyMatrix4(rotationMatrix);
|
||
camera.position.copy(targetPoint).add(direction);
|
||
camera.lookAt(targetPoint);
|
||
|
||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
||
});
|
||
|
||
canvas.addEventListener('mouseup', () => { isDragging = false; });
|
||
|
||
canvas.addEventListener('wheel', (e) => {
|
||
e.preventDefault();
|
||
const targetPoint = new THREE.Vector3(0, 0, 0);
|
||
const direction = camera.position.clone().sub(targetPoint).normalize();
|
||
const distance = camera.position.distanceTo(targetPoint);
|
||
const newDistance = distance + e.deltaY * 20;
|
||
camera.position.copy(targetPoint).addScaledVector(direction, newDistance);
|
||
});
|
||
|
||
// 렌더링
|
||
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>
|