Files
cadquos-main/public/assets/js/lib/dxf-viewer.js

575 lines
20 KiB
JavaScript

// DXF Viewer for Gitea - 캐시 무효화 버전 (2025-06-27 08:00)
// Using Three.js and dxf-parser
// 캐시버스팅을 위한 고유 주석
// 메인 뷰어 클래스
/* 업데이트 버전: 2025-06-27 - drawEntity 문제 수정 */
class DXFViewer {
constructor(container) {
console.log('뷰어 초기화 시작, 컨테이너:', container);
console.log('컨테이너 크기:', container.clientWidth, 'x', container.clientHeight);
this.container = container;
this.dxfGroup = new THREE.Group();
this.init();
// 디버그용 직접 테스트 호출
this.addTestObjects();
}
init() {
console.log('초기화 범위:', this.container.offsetWidth, 'x', this.container.offsetHeight);
// 씬 초기화
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xf0f0f0);
// 카메라 초기화
const width = this.container.offsetWidth || window.innerWidth;
const height = this.container.offsetHeight || window.innerHeight;
const aspect = width / height;
console.log('화면 비율:', aspect, '크기:', width, 'x', height);
this.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
this.camera.position.set(10, 10, 10); // 더 가까운 초기 거리로 설정
this.camera.lookAt(0, 0, 0);
// 렌더러 초기화 - 크기와 스타일 명확히 설정
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(window.devicePixelRatio);
// 렌더러 요소 추가 및 스타일 설정
const canvas = this.renderer.domElement;
canvas.style.display = 'block';
this.container.innerHTML = ''; // 기존 콘텐츠 제거
this.container.appendChild(canvas);
// 조명 추가
const ambientLight = new THREE.AmbientLight(0x404040, 0.7); // 밝기 증가
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1).normalize();
this.scene.add(directionalLight);
// 카메라 위치 설정
this.camera.position.set(50, 50, 50);
this.camera.lookAt(0, 0, 0);
// OrbitControls 초기화 - 여러 방법 시도
console.log('OrbitControls 초기화 시도 (버전 08:03)...');
try {
// 방법 1: 일반 OrbitControls 사용
if (typeof OrbitControls !== 'undefined') {
console.log('방법 1: 일반 OrbitControls 사용');
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.25;
this.controls.enableZoom = true;
this.controls.minDistance = 1;
this.controls.maxDistance = 500;
console.log('OrbitControls 초기화 성공 (방법 1)');
}
// 방법 2: THREE.OrbitControls 사용
else if (THREE && THREE.OrbitControls) {
console.log('방법 2: THREE.OrbitControls 사용');
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.25;
this.controls.enableZoom = true;
this.controls.minDistance = 1;
this.controls.maxDistance = 500;
console.log('OrbitControls 초기화 성공 (방법 2)');
}
// 실패 시 로그만 출력
else {
console.warn('OrbitControls를 찾을 수 없습니다. 기본 모드로 실행합니다.');
}
} catch (error) {
console.error('OrbitControls 초기화 오류:', error);
}
// OrbitControls 상태 확인 및 로그 출력
if (this.controls) {
console.log('OrbitControls 성공적으로 초기화됨');
} else {
console.warn('OrbitControls 초기화 실패, 기본 카메라 컨트롤만 사용');
}
// dxfGroup 초기화
this.dxfGroup = new THREE.Group();
this.scene.add(this.dxfGroup);
// 애니메이션 시작
console.log('애니메이션 루프 시작...');
this.animate();
// 윈도우 크기 조절 이벤트 처리
console.log('윈도우 리사이즈 이벤트 리스너 추가');
window.addEventListener('resize', this.onWindowResize.bind(this), false);
}
onWindowResize() {
console.log('윈도우 크기 변경 처리 중...');
const width = this.container.offsetWidth || window.innerWidth;
const height = this.container.offsetHeight || window.innerHeight;
console.log('새 크기:', width, 'x', height);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
// 즉시 렌더링 강제
this.renderer.render(this.scene, this.camera);
}
animate() {
// 너무 많은 로그가 발생하므로 첫 10프레임에만 로그 출력
if (!this._frameCount) this._frameCount = 0;
if (this._frameCount < 10) {
console.log(`애니메이션 프레임 #${this._frameCount}`);
this._frameCount++;
}
requestAnimationFrame(this.animate.bind(this));
if (this.controls) {
this.controls.update();
}
this.renderer.render(this.scene, this.camera);
}
// 테스트용 객체 추가
addTestObjects() {
console.log('테스트 객체 추가 중...');
// 그리드 헬퍼 추가
const gridHelper = new THREE.GridHelper(20, 20, 0x0000ff, 0x808080);
this.scene.add(gridHelper);
// 축 헬퍼 추가
const axesHelper = new THREE.AxesHelper(10);
this.scene.add(axesHelper);
// 테스트용 큐브 추가
const geometry = new THREE.BoxGeometry(5, 5, 5);
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
wireframe: true,
wireframeLinewidth: 2
});
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 2.5, 0); // 그리드 위에 위치
this.scene.add(cube);
console.log('테스트 객체 추가 완료');
// 즉시 렌더링
this.renderer.render(this.scene, this.camera);
}
loadDXF(url, options = {}) {
// 기존 DXF 엔티티 제거
this.clearDXF();
console.log('DXF 파일 로드 중...', url);
// dxf-parser 라이브러리는 'window.DxfParser'로 전역 스코프에 추가됩니다
if (!window.DxfParser) {
console.error('DxfParser 라이브러리를 찾을 수 없습니다');
if (options.onError) options.onError(new Error('DxfParser 라이브러리를 찾을 수 없습니다'));
return;
}
const parser = new window.DxfParser();
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP 오류! 상태: ${response.status}`);
}
console.log('파일 다운로드 성공');
return response.text();
})
.then(data => {
console.log('DXF 파일 크기:', data.length, '바이트');
try {
console.log('DXF 파싱 시작...');
const dxf = parser.parseSync(data);
console.log('DXF 파싱 완료, 엔티티 수:', dxf.entities ? dxf.entities.length : 0);
this.renderDXF(dxf);
// 성공 콜백 호출
if (options.onSuccess) options.onSuccess(dxf);
} catch (error) {
console.error('DXF 파싱 오류:', error);
if (options.onError) options.onError(error);
}
})
.catch(error => {
console.error('DXF 로딩 오류:', error);
if (options.onError) options.onError(error);
});
}
clearDXF() {
// 그리드와 조명을 제외한 모든 객체 제거
this.scene.children = this.scene.children.filter(child =>
child instanceof THREE.GridHelper ||
child instanceof THREE.Light
);
}
renderDXF(dxf) {
console.log('DXF 렌더링 시작 - 최신 버전 (캐시버스팅 08:00)...');
console.log('이 문구가 콘솔에 표시되면 최신 버전이 로드된 것입니다.');
console.log('DXF 객체 내용:', dxf);
if (!dxf || !dxf.entities || !Array.isArray(dxf.entities)) {
console.error('DXF 파일에 유효한 엔티티가 없습니다.');
return;
}
console.log('엔티티 수:', dxf.entities.length);
console.log('엔티티 유형:', dxf.entities.map(e => e.type).filter((v, i, a) => a.indexOf(v) === i));
// 범위 계산을 위한 변수 초기화
const min = new THREE.Vector3(Infinity, Infinity, Infinity);
const max = new THREE.Vector3(-Infinity, -Infinity, -Infinity);
// 엔티티 로드
let loadedEntityCount = 0;
let skippedEntities = 0;
let entityTypeCount = {};
// 각 엔티티 처리 디버깅 추가
dxf.entities.forEach((entity, index) => {
if (index < 10) {
console.log(`엔티티 ${index} 정보:`, JSON.stringify(entity).substring(0, 200) + '...');
}
// 엔티티 유형 카운트
entityTypeCount[entity.type] = (entityTypeCount[entity.type] || 0) + 1;
try {
const object = this.createObjectFromEntity(entity);
if (object) {
this.dxfGroup.add(object);
loadedEntityCount++;
// 범위 계산 업데이트
if (object.geometry) {
object.geometry.computeBoundingBox();
min.min(object.geometry.boundingBox.min);
max.max(object.geometry.boundingBox.max);
}
} else {
skippedEntities++;
}
} catch (err) {
console.error(`엔티티 ${index} (${entity.type}) 처리 오류:`, err);
skippedEntities++;
}
});
console.log('로드된 엔티티 수:', loadedEntityCount);
console.log('챴너뜨린 엔티티 수:', skippedEntities);
console.log('엔티티 유형별 통계:', entityTypeCount);
console.log('범위:', 'min:', min.toArray(), 'max:', max.toArray());
// 더미 오브젝트 추가 - 테스트용
console.log('더미 오브젝트 추가...');
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true });
const cube = new THREE.Mesh(geometry, material);
this.dxfGroup.add(cube);
// 좌표축 생성
const axesHelper = new THREE.AxesHelper(20);
this.dxfGroup.add(axesHelper);
this.scene.add(this.dxfGroup);
// 카메라 위치 조정
if (loadedEntityCount > 0) {
// 바운딩 박스 계산
const bbox = {
min: new THREE.Vector3(min.x, min.y, min.z),
max: new THREE.Vector3(max.x, max.y, max.z)
};
// 유효한 바운딩 박스인지 확인
if (isFinite(min.x) && isFinite(max.x) && isFinite(min.y) && isFinite(max.y)) {
this.fitCameraToObject(this.dxfGroup, bbox);
} else {
console.warn('유효한 범위가 계산되지 않음, 기본 카메라 뷰 사용');
// 기본 뷰 사용
this.camera.position.set(50, 50, 50);
this.camera.lookAt(0, 0, 0);
}
} else {
// 엔티티가 없는 경우 기본 뷰 사용
this.camera.position.set(50, 50, 50);
this.camera.lookAt(0, 0, 0);
}
console.log('DXF 렌더링 완료');
}
getBoundingBox(entities) {
// DXF 엔티티의 바운딩 박스 계산
const bbox = {
min: { x: Infinity, y: Infinity, z: Infinity },
max: { x: -Infinity, y: -Infinity, z: -Infinity }
};
entities.forEach(entity => {
if (entity.vertices) {
entity.vertices.forEach(vertex => {
bbox.min.x = Math.min(bbox.min.x, vertex.x);
bbox.min.y = Math.min(bbox.min.y, vertex.y);
bbox.min.z = Math.min(bbox.min.z, vertex.z || 0);
bbox.max.x = Math.max(bbox.max.x, vertex.x);
bbox.max.y = Math.max(bbox.max.y, vertex.y);
bbox.max.z = Math.max(bbox.max.z, vertex.z || 0);
});
} else if (entity.position) {
bbox.min.x = Math.min(bbox.min.x, entity.position.x);
bbox.min.y = Math.min(bbox.min.y, entity.position.y);
bbox.min.z = Math.min(bbox.min.z, entity.position.z || 0);
bbox.max.x = Math.max(bbox.max.x, entity.position.x);
bbox.max.y = Math.max(bbox.max.y, entity.position.y);
bbox.max.z = Math.max(bbox.max.z, entity.position.z || 0);
}
});
// 바운딩 박스가 너무 작은 경우 기본값 설정
if (!isFinite(bbox.min.x)) {
bbox.min = { x: -10, y: -10, z: -10 };
bbox.max = { x: 10, y: 10, z: 10 };
}
return bbox;
}
createObjectFromEntity(entity) {
const material = new THREE.LineBasicMaterial({ color: 0x0000ff });
switch (entity.type) {
case 'LINE':
return this.createLine(entity, material);
case 'CIRCLE':
return this.createCircle(entity, material);
case 'ARC':
return this.createArc(entity, material);
case 'POLYLINE':
case 'LWPOLYLINE':
return this.createPolyline(entity, material);
case 'TEXT':
// 텍스트는 현재 구현하지 않음
return null;
default:
console.log('미지원 엔티티 타입:', entity.type);
return null;
}
}
createLine(entity, material) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(entity.vertices[0].x, entity.vertices[0].y, entity.vertices[0].z || 0),
new THREE.Vector3(entity.vertices[1].x, entity.vertices[1].y, entity.vertices[1].z || 0)
]);
return new THREE.Line(geometry, material);
}
createCircle(entity, material) {
const curve = new THREE.EllipseCurve(
entity.center.x, entity.center.y,
entity.radius, entity.radius,
0, 2 * Math.PI,
false,
0
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
createArc(entity, material) {
const startAngle = entity.startAngle;
const endAngle = entity.endAngle;
const curve = new THREE.EllipseCurve(
entity.center.x, entity.center.y,
entity.radius, entity.radius,
startAngle, endAngle,
false,
0
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
createPolyline(entity, material) {
const points = entity.vertices.map(vertex =>
new THREE.Vector3(vertex.x, vertex.y, vertex.z || 0)
);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
fitCameraToObject(object, bbox) {
// 바운딩 박스 안전 검사
if (!bbox || !bbox.min || !bbox.max) {
console.warn('유효한 바운딩 박스가 없습니다. 기본 카메라 뷰 사용');
// 기본 카메라 위치 사용
this.camera.position.set(50, 50, 50);
this.camera.lookAt(0, 0, 0);
if (this.controls) {
this.controls.target.set(0, 0, 0);
this.controls.update();
}
return;
}
// controls가 없는 경우 처리
if (!this.controls) {
console.warn('OrbitControls가 정의되지 않았습니다. 기본 카메라 설정 사용');
this.camera.position.set(bbox.max.x, bbox.max.y, bbox.max.z * 3);
this.camera.lookAt(new THREE.Vector3(
(bbox.min.x + bbox.max.x) / 2,
(bbox.min.y + bbox.max.y) / 2,
(bbox.min.z + bbox.max.z) / 2
));
return;
}
const size = new THREE.Vector3(
bbox.max.x - bbox.min.x,
bbox.max.y - bbox.min.y,
bbox.max.z - bbox.min.z
);
const center = new THREE.Vector3(
(bbox.min.x + bbox.max.x) / 2,
(bbox.min.y + bbox.max.y) / 2,
(bbox.min.z + bbox.max.z) / 2
);
// 객체가 너무 작거나 큰 경우 조정
const maxSize = Math.max(size.x, size.y, size.z);
const fitHeightDistance = maxSize / (2 * Math.atan(Math.PI * this.camera.fov / 360));
const fitWidthDistance = fitHeightDistance / this.camera.aspect;
const distance = 1.2 * Math.max(fitHeightDistance, fitWidthDistance);
// 카메라 위치 설정
const direction = this.controls.target.clone()
.sub(this.camera.position)
.normalize()
.multiplyScalar(distance);
this.controls.maxDistance = distance * 10;
this.camera.position.copy(center).sub(direction);
this.controls.target.copy(center);
// 컨트롤 업데이트
this.camera.near = distance / 100;
this.camera.far = distance * 100;
this.camera.updateProjectionMatrix();
this.controls.update();
}
}
// 페이지 초기화
document.addEventListener('DOMContentLoaded', () => {
// 디버깅 정보 출력
console.log('DXF 뷰어 초기화 시작');
// 지정된 컨테이너를 찾아서 뷰어 초기화
const container = document.getElementById('dxf-viewer-container');
if (!container) {
console.error('dxf-viewer-container를 찾을 수 없습니다');
return;
}
console.log('컨테이너를 찾음:', container);
const viewer = new DXFViewer(container);
// 다른 뷰어 데이터 소스 시도
const viewerData = document.getElementById('dxf-viewer-data');
if (viewerData && viewerData.getAttribute('data-url')) {
const fileURL = viewerData.getAttribute('data-url');
console.log('dxf-viewer-data에서 URL 찾음:', fileURL);
viewer.loadDXF(fileURL);
}
// dxf-file-url 메타 태그에서 URL 가져오기
console.log('메타 태그에서 DXF 파일 URL 검색 중...');
const fileURLMeta = document.querySelector('meta[name="dxf-file-url"]');
if (fileURLMeta) {
const fileURL = fileURLMeta.getAttribute('content');
console.log('메타 태그에서 찾은 URL:', fileURL);
if (fileURL && fileURL.trim() !== '') {
console.log('유효한 URL 발견, DXF 파일 로드 시작');
// URL을 사용하여 DXF 파일 로드
viewer.loadDXF(fileURL, {
onSuccess: (dxf) => {
console.log('DXF 파일 로드 성공:', fileURL);
console.log('DXF 엔티티 수:', dxf.entities ? dxf.entities.length : 0);
},
onError: (error) => {
console.error('DXF 파일 로드 실패:', error);
// 실패 시 추가적인 방법 시도
console.log('대체 URL 형식으로 시도 중...');
// 기본 URL에서 구성된 대체 URL 생성
const commitIdMeta = document.querySelector('meta[name="dxf-commit-id"]');
const repoPathMeta = document.querySelector('meta[name="dxf-repo-path"]');
const repoLinkMeta = document.querySelector('meta[name="dxf-repo-link"]');
if (commitIdMeta && repoPathMeta && repoLinkMeta) {
const commitId = commitIdMeta.getAttribute('content');
const repoPath = repoPathMeta.getAttribute('content');
const repoLink = repoLinkMeta.getAttribute('content');
if (commitId && repoPath && repoLink) {
const alternativeURL = `/api/v1/repos${repoLink}/raw/${repoPath}?ref=${commitId}`;
console.log('대체 URL:', alternativeURL);
// 대체 URL로 로드 시도
viewer.loadDXF(alternativeURL, {
onSuccess: (dxf) => console.log('대체 URL 성공:', dxf.entities ? dxf.entities.length : 0),
onError: (err) => console.error('대체 URL 로드 실패:', err)
});
}
}
}
});
} else {
console.error('dxf-file-url 메타 태그의 content가 비어 있습니다');
}
} else {
console.error('dxf-file-url 메타 태그를 찾을 수 없습니다');
}
});