전체 스크롤 제거: 1. html, body: overflow: hidden 2. .container: overflow: hidden 3. #canvas: overflow: hidden 4. 패널 외부 스크롤 제거 내부 스크롤만 활성화: - 패널 내용이 넘칠 때만 패널 내부 스크롤 표시 - 커스텀 스크롤바 스타일 적용 결과: - 화면에 맞춘 레이아웃 - 우측/하단 스크롤 없음 - 화면비율(16:9 등) 완벽 대응 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
504 lines
17 KiB
HTML
504 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>
|
||
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 > * {
|
||
flex-shrink: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.panel > :not(:last-child) {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.panel > :nth-last-child(1) {
|
||
margin-top: auto;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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: 11px;
|
||
color: #cbd5e0;
|
||
margin: 3px 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;
|
||
}
|
||
|
||
</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 src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/controls/OrbitControls.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)
|
||
|
||
// 먼저 샘플 부재 1개로 테스트
|
||
console.log('=== 샘플 부재 테스트 ===');
|
||
const testGeometry = new THREE.BoxGeometry(4, 4, 80); // 400×400×8000mm → 4×4×80 (축소)
|
||
const testMaterial = new THREE.MeshStandardMaterial({ color: 0x7ed321, roughness: 0.7 });
|
||
const testMesh = new THREE.Mesh(testGeometry, testMaterial);
|
||
testMesh.position.set(470, -2830, 100); // 첫 번째 h-pile2 좌표
|
||
scene.add(testMesh);
|
||
console.log('✓ 샘플 부재 추가됨 @ (470, -2830)');
|
||
|
||
// 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(3000, 20, 0x444444, 0x222222);
|
||
gridHelper.position.y = -2300;
|
||
scene.add(gridHelper);
|
||
|
||
const axisHelper = new THREE.AxesHelper(500);
|
||
scene.add(axisHelper);
|
||
|
||
// OrbitControls로 카메라 제어
|
||
const controls = new THREE.OrbitControls(camera, canvas);
|
||
controls.target.set(-216, -2554, 0); // 부재 중심을 타겟으로
|
||
controls.autoRotate = false;
|
||
controls.autoRotateSpeed = 2;
|
||
controls.enableDamping = true;
|
||
controls.dampingFactor = 0.05;
|
||
controls.enableZoom = true;
|
||
controls.zoomSpeed = 1.0;
|
||
controls.enablePan = true;
|
||
controls.panSpeed = 0.5;
|
||
controls.update();
|
||
|
||
// 렌더링
|
||
function animate() {
|
||
requestAnimationFrame(animate);
|
||
controls.update();
|
||
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>
|