model tree
Build and Push Docker Image / build (push) Successful in 13s

This commit is contained in:
jason
2026-04-23 13:46:54 -05:00
parent a262d4f018
commit ea1c7f6e7f
4 changed files with 162 additions and 5 deletions
+114 -3
View File
@@ -2,7 +2,7 @@ import '../style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { STLLoader } from 'three/addons/loaders/STLLoader.js'
import type { GeometryFile } from '../../server/services/stepConverter'
import type { GeometryFile, HierarchyNode } from '../../server/services/stepConverter'
// ---- Data injected by viewer.ejs -----------------------------------------
@@ -22,6 +22,9 @@ let camera: THREE.PerspectiveCamera
let controls: OrbitControls
let viewingDist: number = 200 // updated by fitCamera; used by fog toggle
// Map from geometry-JSON mesh index → Three.js Mesh, used by the tree panel
const meshObjects = new Map<number, THREE.Mesh>()
// ---- UI helpers ----------------------------------------------------------
function setLoading(msg: string) {
@@ -244,7 +247,8 @@ async function loadStepGeometry(): Promise<void> {
const group = new THREE.Group()
const isV2 = data.version >= 2
for (const mesh of data.meshes) {
for (let i = 0; i < data.meshes.length; i++) {
const mesh = data.meshes[i]
const geo = new THREE.BufferGeometry()
let positions: Float32Array
@@ -273,12 +277,16 @@ async function loadStepGeometry(): Promise<void> {
geo.computeVertexNormals()
}
group.add(new THREE.Mesh(geo, makeMaterial(mesh.color)))
const threeMesh = new THREE.Mesh(geo, makeMaterial(mesh.color))
meshObjects.set(i, threeMesh)
group.add(threeMesh)
}
scene.add(group)
fitCamera(group)
addGrid(group)
if (data.tree) wireTreePanel(data.tree)
}
// ---- STL loader (client-side, Three.js built-in) -------------------------
@@ -318,6 +326,109 @@ async function loadStl(): Promise<void> {
addGrid(mesh)
}
// ---- Model tree ----------------------------------------------------------
function setNodeVisible(node: HierarchyNode, visible: boolean) {
for (const idx of node.meshes) {
const m = meshObjects.get(idx)
if (m) m.visible = visible
}
for (const child of node.children) setNodeVisible(child, visible)
}
function buildTreeNode(node: HierarchyNode, depth: number): HTMLElement {
const label = node.name.trim() || 'Solid'
const hasChildren = node.children.length > 0
const wrapper = document.createElement('div')
const row = document.createElement('div')
row.className = 'flex items-center gap-1 py-1 rounded-lg hover:bg-white/5 group select-none'
row.style.paddingLeft = `${6 + depth * 14}px`
row.style.paddingRight = '6px'
// Chevron
const chevron = document.createElement('span')
chevron.className = `w-3.5 h-3.5 shrink-0 text-gray-600 transition-transform duration-150 ${hasChildren ? 'cursor-pointer' : 'invisible'}`
chevron.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>`
// Eye toggle
const EYE_ON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>`
const EYE_OFF = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>`
const eyeBtn = document.createElement('button')
eyeBtn.className = 'w-3.5 h-3.5 shrink-0 text-gray-500 hover:text-white transition-colors opacity-0 group-hover:opacity-100'
eyeBtn.title = 'Toggle visibility'
eyeBtn.innerHTML = EYE_ON
let visible = true
eyeBtn.addEventListener('click', (e) => {
e.stopPropagation()
visible = !visible
eyeBtn.innerHTML = visible ? EYE_ON : EYE_OFF
eyeBtn.style.opacity = visible ? '' : '1'
eyeBtn.style.color = visible ? '' : 'rgb(248 113 113)'
nameEl.style.opacity = visible ? '' : '0.3'
setNodeVisible(node, visible)
})
// Name
const nameEl = document.createElement('span')
nameEl.className = 'text-xs text-gray-300 truncate flex-1'
nameEl.textContent = label
row.append(chevron, eyeBtn, nameEl)
wrapper.append(row)
if (hasChildren) {
const childrenEl = document.createElement('div')
for (const child of node.children) {
childrenEl.append(buildTreeNode(child, depth + 1))
}
let expanded = true
chevron.style.transform = 'rotate(90deg)'
const toggle = () => {
expanded = !expanded
chevron.style.transform = expanded ? 'rotate(90deg)' : ''
childrenEl.style.display = expanded ? '' : 'none'
}
chevron.addEventListener('click', (e) => { e.stopPropagation(); toggle() })
row.addEventListener('click', toggle)
wrapper.append(childrenEl)
}
return wrapper
}
function wireTreePanel(root: HierarchyNode) {
const content = document.getElementById('tree-content')
if (!content) return
// Skip an unnamed single-child root wrapper — go straight to its children
const topNodes = (!root.name && root.children.length > 0) ? root.children : [root]
content.innerHTML = ''
for (const node of topNodes) {
content.append(buildTreeNode(node, 0))
}
// Wire tree toggle button
const treePanel = document.getElementById('tree-panel')!
const toggleBtn = document.getElementById('tree-toggle-btn')
const closeBtn = document.getElementById('tree-close-btn')
const openPanel = () => { treePanel.classList.remove('closed'); toggleBtn?.classList.add('text-accent') }
const closePanel = () => { treePanel.classList.add('closed'); toggleBtn?.classList.remove('text-accent') }
toggleBtn?.addEventListener('click', () =>
treePanel.classList.contains('closed') ? openPanel() : closePanel()
)
closeBtn?.addEventListener('click', closePanel)
}
// ---- Viewer toolbar ------------------------------------------------------
function wireToolbar() {