LH 정리된 도면 기반 3D 모델 생성 완료
완성 내용: ✅ LH 도면 분석 및 부재 추출 ✅ 710개 부재 자동 식별 ✅ 레이어 기반 명확한 구조화 ✅ 웹 3D 뷰어 생성 파일 생성: - src/build/generate-3d-lh.mjs (기본 버전) - src/build/generate-3d-lh-full.mjs (전체 버전) - out/가시설계획_LH-full.obj (3D 모델) - out/가시설계획_LH-full-volumes.json (부재 정보) - out/viewer-lh.html (웹 뷰어) 부재 통계: - 총 부재: 710개 - 토층 (지질): 580개 - 하천 구조물: 52개 - 버팀목: 42개 - 단면표시: 32개 - 기초 파일: 4개 - 총 부피: 31,596 m³ 레이어 구조 (정리됨): ✓ [가시설]: 700개 선 + 원 + 폴리라인 ✓ [기존구조물]: 626개 선 (하천 구조물) ✓ [지질주상도]: 580개 원 + 폴리라인 (지질층) ✓ [주상도]: 단면도 정보 축척: 1:120 (이전 1:200 보다 상세) 좌표범위: X 278,189 × Y 281,730 장점: 1. 레이어가 명확하게 정리됨 2. 5,646개 엔티티로 더 상세 3. 자동 부재 추출 가능 4. 웹 뷰어로 실시간 확인 가능 결론: 레이어가 정리된 도면은 자동화 가능하고, 더 정확하며 완성도가 높습니다. 파이프라인 검증 완료: ✓ 가시설_평면.dxf (복잡한 도면) ✓ 가시설계획_LH.dxf (정리된 도면) ← 권장 다음: 다른 도면 적용 또는 배포 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78646b1098
commit
22346da96f
9 files changed
+64473
-1
No files matched your search
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-3d-lh-full.mjs - LH 도면 전체 3D 모델 생성 (상세 버전)
|
||||
*/
|
||||
|
||||
import DxfParser from 'dxf-parser';
|
||||
import * as THREE from 'three';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const SCALE = 120; // 축척 1:120
|
||||
|
||||
async function generate3DLHFull(filename = '가시설계획_LH') {
|
||||
console.log(`\n[generate-3d-lh-full] ${filename} - 상세 3D 모델 생성\n`);
|
||||
|
||||
try {
|
||||
const dxfPath = path.join('RAG/LH', `${filename}.dxf`);
|
||||
const dxfContent = fs.readFileSync(dxfPath, 'utf-8');
|
||||
const parser = new DxfParser();
|
||||
const dxf = parser.parse(dxfContent);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const allParts = [];
|
||||
let totalVolume = 0;
|
||||
|
||||
console.log('=== 부재 추출 ===\n');
|
||||
|
||||
// 1. [가시설] - 주요 부재 (모두 처리)
|
||||
console.log('[1] [가시설] 주요 부재\n');
|
||||
const gasiLines = dxf.entities.filter(e => e.layer === '가시설' && e.type === 'LINE');
|
||||
const gasiCircles = dxf.entities.filter(e => e.layer === '가시설' && e.type === 'CIRCLE');
|
||||
const gasiPolys = dxf.entities.filter(e => e.layer === '가시설' && e.type === 'LWPOLYLINE');
|
||||
|
||||
// 원 처리 (기초 파일)
|
||||
gasiCircles.forEach((circle, idx) => {
|
||||
const geometry = new THREE.CylinderGeometry(250, 250, 2000, 16);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x4a90e2, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const x = circle.center.x * SCALE;
|
||||
const y = circle.center.y * SCALE;
|
||||
mesh.position.set(x, y, 1000);
|
||||
|
||||
const volume = Math.round(Math.PI * 250 * 250 * 2000 / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Foundation-Pile',
|
||||
spec: 'φ500×2000',
|
||||
dimensions: 'φ500×2000mm',
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
});
|
||||
|
||||
console.log(` 원(파일): ${gasiCircles.length}개 추가`);
|
||||
|
||||
// 선 처리 (버팀목/보)
|
||||
gasiLines.forEach((line, idx) => {
|
||||
if (!line.vertices || line.vertices.length < 2) return;
|
||||
|
||||
const dx = (line.vertices[1].x - line.vertices[0].x) * SCALE;
|
||||
const dy = (line.vertices[1].y - line.vertices[0].y) * SCALE;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length > 500) { // 500mm 이상만
|
||||
const geometry = new THREE.BoxGeometry(200, 200, length);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0xff6b6b, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const midX = ((line.vertices[0].x + line.vertices[1].x) / 2) * SCALE;
|
||||
const midY = ((line.vertices[0].y + line.vertices[1].y) / 2) * SCALE;
|
||||
mesh.position.set(midX, midY, length / 2);
|
||||
|
||||
const volume = Math.round((200 * 200 * length) / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Strut',
|
||||
spec: '200×200',
|
||||
dimensions: `200×200×${Math.round(length)}mm`,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` 선(버팀목): ${gasiLines.filter(l => {
|
||||
const dx = (l.vertices[1].x - l.vertices[0].x) * SCALE;
|
||||
const dy = (l.vertices[1].y - l.vertices[0].y) * SCALE;
|
||||
return Math.sqrt(dx * dx + dy * dy) > 500;
|
||||
}).length}개 추가`);
|
||||
|
||||
// 폴리라인 처리 (패널)
|
||||
gasiPolys.forEach((poly, idx) => {
|
||||
if (!poly.vertices || poly.vertices.length < 3) return;
|
||||
|
||||
let minX = Infinity, maxX = -Infinity;
|
||||
let minY = Infinity, maxY = -Infinity;
|
||||
|
||||
poly.vertices.forEach(pt => {
|
||||
minX = Math.min(minX, pt.x);
|
||||
maxX = Math.max(maxX, pt.x);
|
||||
minY = Math.min(minY, pt.y);
|
||||
maxY = Math.max(maxY, pt.y);
|
||||
});
|
||||
|
||||
const width = (maxX - minX) * SCALE;
|
||||
const height = (maxY - minY) * SCALE;
|
||||
|
||||
if (width > 100 && height > 100) {
|
||||
const geometry = new THREE.BoxGeometry(width, height, 300);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x7ed321, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const centerX = ((minX + maxX) / 2) * SCALE;
|
||||
const centerY = ((minY + maxY) / 2) * SCALE;
|
||||
mesh.position.set(centerX, centerY, 150);
|
||||
|
||||
const volume = Math.round((width * height * 300) / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Retaining-Panel',
|
||||
dimensions: `${Math.round(width)}×${Math.round(height)}×300mm`,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` 폴리라인(패널): ${gasiPolys.length}개 추가`);
|
||||
|
||||
// 2. [기존구조물] - 하천 구조물 (모두)
|
||||
console.log(`\n[2] [기존구조물] 하천 구조물\n`);
|
||||
const existLines = dxf.entities.filter(e => e.layer === '기존구조물' && e.type === 'LINE');
|
||||
|
||||
const processedExist = existLines.filter(line => {
|
||||
if (!line.vertices || line.vertices.length < 2) return false;
|
||||
const dx = (line.vertices[1].x - line.vertices[0].x) * SCALE;
|
||||
const dy = (line.vertices[1].y - line.vertices[0].y) * SCALE;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
return length > 1000;
|
||||
});
|
||||
|
||||
processedExist.forEach((line) => {
|
||||
const dx = (line.vertices[1].x - line.vertices[0].x) * SCALE;
|
||||
const dy = (line.vertices[1].y - line.vertices[0].y) * SCALE;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const geometry = new THREE.BoxGeometry(200, 200, length);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0xf5a623, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const midX = ((line.vertices[0].x + line.vertices[1].x) / 2) * SCALE;
|
||||
const midY = ((line.vertices[0].y + line.vertices[1].y) / 2) * SCALE;
|
||||
mesh.position.set(midX, midY, length / 2);
|
||||
|
||||
const volume = Math.round((200 * 200 * length) / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'River-Structure',
|
||||
dimensions: `200×200×${Math.round(length)}mm`,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
});
|
||||
|
||||
console.log(` 대형 구조물: ${processedExist.length}개 추가`);
|
||||
|
||||
// 3. [지질주상도] - 지질층 (모두)
|
||||
console.log(`\n[3] [지질주상도] 지질층\n`);
|
||||
const geoCircles = dxf.entities.filter(e => e.layer === '지질주상도' && e.type === 'CIRCLE');
|
||||
|
||||
geoCircles.forEach((circle, idx) => {
|
||||
if (!circle.center) return;
|
||||
|
||||
const geometry = new THREE.CylinderGeometry(150, 150, 500, 8);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0xd4a574, roughness: 0.8 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const x = circle.center.x * SCALE;
|
||||
const y = circle.center.y * SCALE;
|
||||
mesh.position.set(x, y, 250);
|
||||
|
||||
const volume = Math.round(Math.PI * 150 * 150 * 500 / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Soil-Layer',
|
||||
diameter: 300,
|
||||
depth: 500,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
});
|
||||
|
||||
console.log(` 토층: ${geoCircles.length}개 추가`);
|
||||
|
||||
// 4. [주상도] - 단면도 정보
|
||||
console.log(`\n[4] [주상도] 단면\n`);
|
||||
const sectionPolys = dxf.entities.filter(e => e.layer === '주상도' && e.type === 'LWPOLYLINE');
|
||||
const sectionCircles = dxf.entities.filter(e => e.layer === '주상도' && e.type === 'CIRCLE');
|
||||
|
||||
sectionCircles.slice(0, 50).forEach((circle) => {
|
||||
if (!circle.center) return;
|
||||
|
||||
const geometry = new THREE.CylinderGeometry(100, 100, 300, 8);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0xbfbfbf, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const x = circle.center.x * SCALE;
|
||||
const y = circle.center.y * SCALE;
|
||||
mesh.position.set(x, y, 150);
|
||||
|
||||
mesh.userData = {
|
||||
type: 'Section-Marker',
|
||||
dimensions: '200×300mm',
|
||||
volume: 1
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
});
|
||||
|
||||
console.log(` 단면표시: ${Math.min(50, sectionCircles.length)}개 추가`);
|
||||
|
||||
// 통계
|
||||
console.log('\n=== 최종 통계 ===\n');
|
||||
console.log(`총 부재: ${allParts.length}개`);
|
||||
console.log(`총 부피: ${totalVolume}m³\n`);
|
||||
|
||||
const typeCount = {};
|
||||
allParts.forEach(part => {
|
||||
const type = part.type;
|
||||
typeCount[type] = (typeCount[type] || 0) + 1;
|
||||
});
|
||||
|
||||
console.log('부재 구성:');
|
||||
Object.entries(typeCount).sort((a, b) => b[1] - a[1]).forEach(([type, count]) => {
|
||||
console.log(` ${type}: ${count}개`);
|
||||
});
|
||||
|
||||
// 파일 저장
|
||||
console.log('\n=== 파일 저장 ===\n');
|
||||
|
||||
const objPath = path.join('out', `${filename}-full.obj`);
|
||||
exportOBJ(scene, objPath);
|
||||
console.log(` ✓ ${objPath}`);
|
||||
|
||||
const volPath = path.join('out', `${filename}-full-volumes.json`);
|
||||
const volumeInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
drawing: filename,
|
||||
method: 'Full-Layer-Analysis',
|
||||
totalVolume: totalVolume,
|
||||
partCount: allParts.length,
|
||||
scale: '1:120',
|
||||
parts: allParts,
|
||||
typeCount,
|
||||
status: 'COMPLETE'
|
||||
};
|
||||
|
||||
fs.writeFileSync(volPath, JSON.stringify(volumeInfo, null, 2));
|
||||
console.log(` ✓ ${volPath}\n`);
|
||||
|
||||
console.log(`[generate-3d-lh-full] ✅ 상세 3D 모델 생성 완료!\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[generate-3d-lh-full] ❌ 오류: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function exportOBJ(scene, filename) {
|
||||
let objContent = '# DXF to 3D (LH Full Model)\n';
|
||||
objContent += `# Generated: ${new Date().toISOString()}\n\n`;
|
||||
|
||||
let vertexIndex = 1;
|
||||
const meshes = [];
|
||||
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
meshes.push(object);
|
||||
}
|
||||
});
|
||||
|
||||
for (const mesh of meshes) {
|
||||
const geometry = mesh.geometry;
|
||||
const position = geometry.getAttribute('position');
|
||||
|
||||
if (position) {
|
||||
for (let i = 0; i < position.count; i++) {
|
||||
const x = position.getX(i);
|
||||
const y = position.getY(i);
|
||||
const z = position.getZ(i);
|
||||
objContent += `v ${x.toFixed(2)} ${y.toFixed(2)} ${z.toFixed(2)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const index = geometry.getIndex();
|
||||
if (index) {
|
||||
for (let i = 0; i < index.count; i += 3) {
|
||||
const a = index.getX(i) + vertexIndex;
|
||||
const b = index.getX(i + 1) + vertexIndex;
|
||||
const c = index.getX(i + 2) + vertexIndex;
|
||||
objContent += `f ${a} ${b} ${c}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
vertexIndex += position ? position.count : 0;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filename, objContent, 'utf-8');
|
||||
}
|
||||
|
||||
await generate3DLHFull('가시설계획_LH');
|
||||
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-3d-lh.mjs - LH 도면용 3D 모델 생성 (레이어 정리 버전)
|
||||
*
|
||||
* 특징: 레이어가 명확하게 정리되어 있음
|
||||
* - [가시설]: 주요 부재
|
||||
* - [가시설평면]: 상세 블록
|
||||
* - [기존구조물]: 하천 구조물
|
||||
* - [지질주상도]: 지질 정보
|
||||
*/
|
||||
|
||||
import DxfParser from 'dxf-parser';
|
||||
import * as THREE from 'three';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const SCALE = 120; // 축척 1:120 (LH 도면)
|
||||
|
||||
async function generate3DLH(filename = '가시설계획_LH') {
|
||||
console.log(`\n[generate-3d-lh] ${filename} - 3D 모델 생성\n`);
|
||||
|
||||
try {
|
||||
// DXF 로드
|
||||
const dxfPath = path.join('RAG/LH', `${filename}.dxf`);
|
||||
const dxfContent = fs.readFileSync(dxfPath, 'utf-8');
|
||||
const parser = new DxfParser();
|
||||
const dxf = parser.parse(dxfContent);
|
||||
|
||||
console.log('=== 부재 추출 ===\n');
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const allParts = [];
|
||||
let totalVolume = 0;
|
||||
|
||||
// 1. [가시설] - 주요 부재
|
||||
console.log('[1] [가시설] 레이어 - 주요 부재\n');
|
||||
const gasiLines = dxf.entities.filter(e => e.layer === '가시설' && e.type === 'LINE');
|
||||
const gasiPolys = dxf.entities.filter(e => e.layer === '가시설' && e.type === 'LWPOLYLINE');
|
||||
|
||||
console.log(` 선: ${gasiLines.length}개`);
|
||||
console.log(` 폴리라인: ${gasiPolys.length}개`);
|
||||
|
||||
// 선들을 높이 순으로 그룹화 (단면)
|
||||
const heightGroups = {};
|
||||
gasiLines.forEach(line => {
|
||||
if (!line.vertices || line.vertices.length < 2) return;
|
||||
const y = Math.round(line.vertices[0].y / 500) * 500; // 500mm 단위로 그룹화
|
||||
const yKey = `Y${y}`;
|
||||
if (!heightGroups[yKey]) heightGroups[yKey] = [];
|
||||
heightGroups[yKey].push(line);
|
||||
});
|
||||
|
||||
let memberCount = 0;
|
||||
Object.entries(heightGroups).slice(0, 20).forEach(([heightKey, lines]) => { // 상위 20개
|
||||
if (lines.length < 3) return;
|
||||
|
||||
// 박스 부재로 표현
|
||||
const lengths = lines.map(l => {
|
||||
const dx = (l.vertices[1].x - l.vertices[0].x) * SCALE;
|
||||
const dy = (l.vertices[1].y - l.vertices[0].y) * SCALE;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
});
|
||||
|
||||
const avgLength = lengths.reduce((a, b) => a + b) / lengths.length;
|
||||
const maxLength = Math.max(...lengths);
|
||||
|
||||
if (maxLength > 500) { // 500mm 이상만
|
||||
const geometry = new THREE.BoxGeometry(300, 300, maxLength);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0xff6b6b, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const centerY = parseInt(heightKey.slice(1));
|
||||
const centerX = lines.reduce((sum, l) => sum + l.vertices[0].x, 0) / lines.length * SCALE;
|
||||
|
||||
mesh.position.set(centerX, centerY, maxLength / 2);
|
||||
|
||||
const volume = Math.round((300 * 300 * maxLength) / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Structural-Member',
|
||||
spec: '300×300 (추정)',
|
||||
dimensions: `300×300×${Math.round(maxLength)}mm`,
|
||||
volume: volume,
|
||||
lineCount: lines.length
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
memberCount++;
|
||||
|
||||
console.log(` ✓ 부재 ${memberCount}: ${lines.length}개 선, ${volume}m³`);
|
||||
}
|
||||
});
|
||||
|
||||
// 폴리라인도 처리
|
||||
gasiPolys.forEach((poly, idx) => {
|
||||
if (!poly.vertices || poly.vertices.length < 3) return;
|
||||
|
||||
let minX = Infinity, maxX = -Infinity;
|
||||
let minY = Infinity, maxY = -Infinity;
|
||||
|
||||
poly.vertices.forEach(pt => {
|
||||
minX = Math.min(minX, pt.x);
|
||||
maxX = Math.max(maxX, pt.x);
|
||||
minY = Math.min(minY, pt.y);
|
||||
maxY = Math.max(maxY, pt.y);
|
||||
});
|
||||
|
||||
const width = (maxX - minX) * SCALE;
|
||||
const height = (maxY - minY) * SCALE;
|
||||
|
||||
if (width > 100 && height > 100) {
|
||||
const geometry = new THREE.BoxGeometry(width, height, 300);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x7ed321, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const centerX = ((minX + maxX) / 2) * SCALE;
|
||||
const centerY = ((minY + maxY) / 2) * SCALE;
|
||||
mesh.position.set(centerX, centerY, 150);
|
||||
|
||||
const volume = Math.round((width * height * 300) / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Retaining-Panel',
|
||||
dimensions: `${Math.round(width)}×${Math.round(height)}×300mm`,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. [기존구조물] - 하천 구조물
|
||||
console.log(`\n[2] [기존구조물] - 하천 구조물\n`);
|
||||
const existLines = dxf.entities.filter(e => e.layer === '기존구조물' && e.type === 'LINE');
|
||||
console.log(` 선: ${existLines.length}개`);
|
||||
|
||||
existLines.slice(0, 10).forEach((line, idx) => {
|
||||
if (!line.vertices || line.vertices.length < 2) return;
|
||||
|
||||
const dx = (line.vertices[1].x - line.vertices[0].x) * SCALE;
|
||||
const dy = (line.vertices[1].y - line.vertices[0].y) * SCALE;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length > 1000 && idx < 20) { // 1000mm 이상, 처음 20개
|
||||
const geometry = new THREE.BoxGeometry(200, 200, length);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x4a90e2, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const midX = ((line.vertices[0].x + line.vertices[1].x) / 2) * SCALE;
|
||||
const midY = ((line.vertices[0].y + line.vertices[1].y) / 2) * SCALE;
|
||||
mesh.position.set(midX, midY, length / 2);
|
||||
|
||||
const volume = Math.round((200 * 200 * length) / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'River-Structure',
|
||||
dimensions: `200×200×${Math.round(length)}mm`,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` ✓ 처음 10개 대형 구조물 추가`);
|
||||
|
||||
// 3. [지질주상도] - 지질층 (원 사용)
|
||||
console.log(`\n[3] [지질주상도] - 지질층\n`);
|
||||
const geoCircles = dxf.entities.filter(e => e.layer === '지질주상도' && e.type === 'CIRCLE');
|
||||
console.log(` 원: ${geoCircles.length}개`);
|
||||
|
||||
geoCircles.slice(0, 30).forEach((circle, idx) => {
|
||||
if (!circle.center) return;
|
||||
|
||||
const geometry = new THREE.CylinderGeometry(200, 200, 500, 8);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0xf5a623, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const x = circle.center.x * SCALE;
|
||||
const y = circle.center.y * SCALE;
|
||||
mesh.position.set(x, y, 250);
|
||||
|
||||
const volume = Math.round(Math.PI * 200 * 200 * 500 / 1e6);
|
||||
mesh.userData = {
|
||||
type: 'Soil-Layer',
|
||||
diameter: 400,
|
||||
depth: 500,
|
||||
volume: volume
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += volume;
|
||||
});
|
||||
|
||||
console.log(` ✓ 지질층 30개 추가`);
|
||||
|
||||
// 4. [가시설평면] - INSERT 블록 (상세)
|
||||
console.log(`\n[4] [가시설평면] - 상세 블록\n`);
|
||||
const blkInserts = dxf.entities.filter(e => e.layer === '가시설평면' && e.type === 'INSERT');
|
||||
console.log(` INSERT: ${blkInserts.length}개`);
|
||||
console.log(` ⓘ 블록 상세는 복잡하므로 위치만 표시`);
|
||||
|
||||
blkInserts.slice(0, 20).forEach((insert, idx) => {
|
||||
if (!insert.position) return;
|
||||
|
||||
// 작은 큐브로 표시 (블록 위치)
|
||||
const geometry = new THREE.BoxGeometry(200, 200, 200);
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x7ed321, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const x = insert.position.x * SCALE;
|
||||
const y = insert.position.y * SCALE;
|
||||
mesh.position.set(x, y, 100);
|
||||
|
||||
mesh.userData = {
|
||||
type: 'Detail-Block',
|
||||
blockName: insert.name || 'Block',
|
||||
dimensions: '200×200×200mm',
|
||||
volume: Math.round((200 * 200 * 200) / 1e6)
|
||||
};
|
||||
|
||||
scene.add(mesh);
|
||||
allParts.push(mesh.userData);
|
||||
totalVolume += mesh.userData.volume;
|
||||
});
|
||||
|
||||
console.log(` ✓ 블록 20개 추가`);
|
||||
|
||||
// 통계
|
||||
console.log('\n=== 최종 통계 ===\n');
|
||||
console.log(`총 부재: ${allParts.length}개`);
|
||||
console.log(`총 부피: ${totalVolume}m³\n`);
|
||||
|
||||
const typeCount = {};
|
||||
allParts.forEach(part => {
|
||||
const type = part.type;
|
||||
typeCount[type] = (typeCount[type] || 0) + 1;
|
||||
});
|
||||
|
||||
console.log('부재 구성:');
|
||||
Object.entries(typeCount).sort((a, b) => b[1] - a[1]).forEach(([type, count]) => {
|
||||
console.log(` ${type}: ${count}개`);
|
||||
});
|
||||
|
||||
// 파일 저장
|
||||
console.log('\n=== 파일 저장 ===\n');
|
||||
|
||||
// OBJ 내보내기
|
||||
const objPath = path.join('out', `${filename}-3d.obj`);
|
||||
exportOBJ(scene, objPath);
|
||||
console.log(` ✓ ${objPath}`);
|
||||
|
||||
// 부재 정보
|
||||
const volumeInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
drawing: filename,
|
||||
method: 'Layer-Based-LH',
|
||||
totalVolume: totalVolume,
|
||||
partCount: allParts.length,
|
||||
scale: `1:${1/SCALE}`,
|
||||
parts: allParts.slice(0, 50), // 첫 50개만
|
||||
typeCount,
|
||||
status: 'READY_FOR_WEB'
|
||||
};
|
||||
|
||||
const volPath = path.join('out', `${filename}-volumes.json`);
|
||||
fs.writeFileSync(volPath, JSON.stringify(volumeInfo, null, 2));
|
||||
console.log(` ✓ ${volPath}\n`);
|
||||
|
||||
console.log(`[generate-3d-lh] ✅ 3D 모델 생성 완료!\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[generate-3d-lh] ❌ 오류: ${error.message}`);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function exportOBJ(scene, filename) {
|
||||
let objContent = '# DXF to 3D (LH Model)\n';
|
||||
objContent += `# Generated: ${new Date().toISOString()}\n\n`;
|
||||
|
||||
let vertexIndex = 1;
|
||||
const meshes = [];
|
||||
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
meshes.push(object);
|
||||
}
|
||||
});
|
||||
|
||||
for (const mesh of meshes) {
|
||||
const geometry = mesh.geometry;
|
||||
const position = geometry.getAttribute('position');
|
||||
|
||||
if (position) {
|
||||
for (let i = 0; i < position.count; i++) {
|
||||
const x = position.getX(i);
|
||||
const y = position.getY(i);
|
||||
const z = position.getZ(i);
|
||||
objContent += `v ${x.toFixed(2)} ${y.toFixed(2)} ${z.toFixed(2)}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const index = geometry.getIndex();
|
||||
if (index) {
|
||||
for (let i = 0; i < index.count; i += 3) {
|
||||
const a = index.getX(i) + vertexIndex;
|
||||
const b = index.getX(i + 1) + vertexIndex;
|
||||
const c = index.getX(i + 2) + vertexIndex;
|
||||
objContent += `f ${a} ${b} ${c}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
vertexIndex += position ? position.count : 0;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filename, objContent, 'utf-8');
|
||||
}
|
||||
|
||||
await generate3DLH('가시설계획_LH');
|
||||
Reference in new issue
Block a user