IFC/CityGML Viewer

IFC/CityGMLビューア v29 修正版

IFC/CityGMLビューア v29
プロパティ・断面表示可能

v29は、 IFC / CityGML のプロパティ確認と断面表示が可能な整理版です。
CityGMLの経緯度座標はメートル相当に変換して表示します。
CityGMLが経緯度座標の場合は、自動的にローカルメートル座標へ変換します。

表示・姿勢

IFCはON推奨です。CityGMLはZを高さ方向として読み込むため、この補正は適用しません。

断面表示

水平断面は、補正後の高さ方向で切ります。スライダーを動かすと断面位置が変わります。

プロパティ表示

3D画面上のIFC要素をクリックすると、Type、ExpressID、Name、GlobalIdなどを上部に表示します。 詳細は「詳細」ボタンで開けます。
初期化中…
IFCプロパティ
IFCプロパティ
部位をクリックしてください。
`; } function openOrUpdateFloatingPropertyWindow() { if (!el.useFloatingProp?.checked) return false; try { if (!propFloatWindow || propFloatWindow.closed) { propFloatWindow = window.open('', 'IFC_PROPERTY_FLOAT_WINDOW', 'width=620,height=760,left=80,top=80,resizable=yes,scrollbars=yes'); } if (!propFloatWindow) return false; propFloatWindow.document.open(); propFloatWindow.document.write(buildFloatingPropertyDocument()); propFloatWindow.document.close(); propFloatWindow.focus(); return true; } catch (e) { log('Floating property window failed', String(e?.stack || e)); return false; } } function showProperties(mesh) { selectedMesh = mesh; if (highlightHelper) { scene.remove(highlightHelper); highlightHelper.geometry?.dispose?.(); highlightHelper.material?.dispose?.(); highlightHelper = null; } if (sectionPlaneMesh) { scene.remove(sectionPlaneMesh); sectionPlaneMesh.geometry?.dispose?.(); sectionPlaneMesh.material?.dispose?.(); sectionPlaneMesh = null; } if (renderer) renderer.clippingPlanes = []; highlightHelper = new THREE.BoxHelper(mesh, 0xff0000); scene.add(highlightHelper); const prop = buildPropertyHtml(mesh); lastPropertyHtml = prop.html; lastPropertyText = prop.text; const shownCompact = showCompactTopProperties(mesh); if (!shownCompact) { el.propBody.innerHTML = prop.html; el.propPanel.style.display = 'block'; if (el.topPropBar) el.topPropBar.style.display = 'none'; } log('Part selected', { expressID: mesh.userData.expressID, typeName: mesh.userData.ifcTypeName, geometryExpressID: mesh.userData.geometryExpressID, compactTopBar: shownCompact }); } function onCanvasClick(event) { if (!modelGroup || !raycaster || !mouse || !camera || !renderer) return; const rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const hits = raycaster.intersectObjects(modelGroup.children, true); if (!hits.length) return; let mesh = hits[0].object; while (mesh && (!mesh.isMesh || mesh.userData?.sourceType === 'citygml-edge')) mesh = mesh.parent; if (mesh && mesh.isMesh) showProperties(mesh); } function openPropertyWindow() { if (!lastPropertyHtml) return; const win = window.open('', '_blank'); if (!win) { setStatus('プロパティ別窓を開けませんでした。ポップアップブロックを確認してください。', false); return; } const html = `IFCプロパティ

IFCプロパティ

${lastPropertyHtml}`; win.document.open(); win.document.write(html); win.document.close(); } function collectFacadeElementSegments(opts) { if (!modelGroup) throw new Error('IFCモデルがまだ読み込まれていません。'); modelGroup.updateMatrixWorld(true); const box = new THREE.Box3().setFromObject(modelGroup); const baseFront = opts.side || 'minY'; const spec = getFaceSpec(opts.face || 'front', baseFront); const depthTol = Number(opts.depthTol ?? 1.0); const minSeg = Number(opts.minSeg ?? 0.03); const snapStep = Number(opts.snapStep ?? 0.05); const useTypeFilter = opts.useTypeFilter !== false; const useBboxBand = opts.useBboxBand !== false; const pickedTypes = opts.pickedTypes || new Set(); const segments = new Map(); let meshSeen = 0, meshTypePassed = 0, meshBandPassed = 0, edgeSegments = 0; const typeCounts = {}; const selectedCounts = {}; const tmpBox = new THREE.Box3(); const p1 = new THREE.Vector3(), p2 = new THREE.Vector3(); const axisVal = spec.bandSide === 'min' ? box.min[spec.bandAxis] : box.max[spec.bandAxis]; modelGroup.traverse(mesh => { if (!mesh.isMesh || !mesh.geometry) return; meshSeen++; const typeName = mesh.userData.ifcTypeName || 'UNKNOWN'; typeCounts[typeName] = (typeCounts[typeName] || 0) + 1; if (useTypeFilter && !pickedTypes.has(typeName)) return; meshTypePassed++; tmpBox.setFromObject(mesh); let bandOK = true; if (useBboxBand) { bandOK = spec.bandSide === 'min' ? (tmpBox.min[spec.bandAxis] <= axisVal + depthTol) : (tmpBox.max[spec.bandAxis] >= axisVal - depthTol); } if (!bandOK) return; meshBandPassed++; selectedCounts[typeName] = (selectedCounts[typeName] || 0) + 1; const fc = facadeClassFromType(typeName); let edges; try { edges = new THREE.EdgesGeometry(mesh.geometry, Number(opts.edgeAngle || 20)); } catch (e) { edges = new THREE.EdgesGeometry(mesh.geometry); } const pos = edges.getAttribute('position'); if (!pos) { edges.dispose(); return; } for (let i = 0; i < pos.count; i += 2) { p1.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(mesh.matrixWorld); p2.set(pos.getX(i + 1), pos.getY(i + 1), pos.getZ(i + 1)).applyMatrix4(mesh.matrixWorld); addSegment2D(segments, projectPointForFace(p1, spec), projectPointForFace(p2, spec), minSeg, snapStep, fc.cls, fc.priority); edgeSegments++; } edges.dispose(); }); const rawArr = Array.from(segments.values()); const arr = simplifySegmentsArray(rawArr, opts); log('Facade element extraction completed', { method: 'IFC type filter + face band + EdgesGeometry projection', face: spec.face, label: spec.label, baseFront, bandAxis: spec.bandAxis, bandSide: spec.bandSide, axisVal, depthTol, snapStep, useTypeFilter, useBboxBand, pickedTypes: Array.from(pickedTypes), meshSeen, meshTypePassed, meshBandPassed, rawEdgeSegments: edgeSegments, rawSegmentCount: rawArr.length, segmentCount: arr.length, edgeAngle: Number(opts.edgeAngle || 20), reduceLines: opts.reduceLines !== false, maxLines: Number(opts.maxLines || 8000), typeCounts, selectedCounts, bbox: { min: { x: box.min.x, y: box.min.y, z: box.min.z }, max: { x: box.max.x, y: box.max.y, z: box.max.z } } }); return { segments: arr, bbox: box, stats: { method: 'facadeElement', face: spec.face, label: spec.label, baseFront, bandAxis: spec.bandAxis, bandSide: spec.bandSide, axisVal, depthTol, snapStep, meshSeen, meshTypePassed, meshBandPassed, rawEdgeSegments: edgeSegments, segmentCount: arr.length } }; } function buildSvgDocument(result, styleOpts={}) { const {segments, bbox, stats} = result; if (!segments.length) throw new Error('線が抽出できませんでした。奥行き厚みを増やすか、「多めに拾う」を試してください。'); const minU = Math.min(...segments.map(s => Math.min(s.u1, s.u2))); const maxU = Math.max(...segments.map(s => Math.max(s.u1, s.u2))); const minV = Math.min(...segments.map(s => Math.min(s.v1, s.v2))); const maxV = Math.max(...segments.map(s => Math.max(s.v1, s.v2))); const w = Math.max(1, maxU - minU); const h = Math.max(1, maxV - minV); const pad = Math.max(w, h) * 0.05; const vb = `${minU - pad} ${-(maxV + pad)} ${w + pad * 2} ${h + pad * 2}`; const lines = segments.map(s => ``).join('\n'); const strokeW = Number(styleOpts.strokeW ?? 0.09); const outerW = Number(styleOpts.outerW ?? Math.max(strokeW * 2, 0.18)); const haloW = Number(styleOpts.haloW ?? Math.max(strokeW * 2.4, strokeW + 0.10)); const title = `${stats.label || '線画'} ${escapeHtml(currentFileName || '')}`; const statText = `face=${stats.face || ''}, depth=${stats.depthTol}m, snap=${stats.snapStep}m, lines=${stats.segmentCount}, stroke=${strokeW}`; return ` ${title}
${title}${escapeHtml(statText)}
${segments.map(s => ``).join('\n')}${lines}
スポンサーリンク
シェアする
棒人間をフォローする
タイトルとURLをコピーしました