Files
NoN-Site/nonpacks/static/js/model_viewer.js
T

149 lines
5.9 KiB
JavaScript

// GeoJSON model viewer for the Models/Textures tab. Uses the shared GeoBuilder
// geometry math (bone transforms + box-UV) and renders world-space cubes with
// Three.js. Textures are served through the gated pack_asset endpoint.
(function () {
let renderer = null;
let scene = null;
let camera = null;
let meshRoot = null;
let rafId = null;
const textureLoader = new THREE.TextureLoader();
const materialCache = {};
function assetUrl(baseUrl, member) {
return baseUrl.replace('ASSET', member);
}
function buildGeometry(size, uv, tw, th) {
const g = GeoBuilder.cubeGeometry(size, uv, tw, th);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(g.positions, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(g.uvs, 2));
geometry.setIndex(g.indices);
return geometry;
}
function materialFor(member, baseUrl) {
if (materialCache[member || '__gray__']) return materialCache[member || '__gray__'];
let material;
if (member) {
const texture = textureLoader.load(assetUrl(baseUrl, member));
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
material = new THREE.MeshStandardMaterial({ map: texture, roughness: 0.9, metalness: 0.0 });
} else {
material = new THREE.MeshStandardMaterial({ color: 0x9a9a9a, roughness: 0.9 });
}
materialCache[member || '__gray__'] = material;
return material;
}
function disposeObject(obj) {
obj.traverse((node) => {
if (node.geometry) node.geometry.dispose();
});
for (const key in materialCache) {
if (materialCache[key].map) materialCache[key].map.dispose();
materialCache[key].dispose();
}
for (const key in materialCache) delete materialCache[key];
}
function render(member, container, baseUrl) {
if (renderer) dispose();
const width = container.clientWidth || 480;
const height = container.clientHeight || 480;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1e1e2e);
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 2000);
camera.position.set(0, 20, 60);
camera.lookAt(0, 12, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio || 1);
container.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const key = new THREE.DirectionalLight(0xffffff, 0.9);
key.position.set(30, 60, 30);
scene.add(key);
const fill = new THREE.DirectionalLight(0xffffff, 0.3);
fill.position.set(-30, 20, -30);
scene.add(fill);
meshRoot = new THREE.Group();
scene.add(meshRoot);
fetch(assetUrl(baseUrl, member), { headers: { 'Accept': 'application/json' } })
.then(r => { if (!r.ok) throw new Error('http'); return r.json(); })
.then(geo => {
const built = GeoBuilder.build(geo);
if (!built.cubes.length) throw new Error('no cubes');
for (const cube of built.cubes) {
const geometry = buildGeometry(cube.size, cube.uv, built.texture_width, built.texture_height);
const mesh = new THREE.Mesh(geometry, materialFor(cube.texture, baseUrl));
mesh.matrix = new THREE.Matrix4().fromArray(cube.matrix);
mesh.matrixAutoUpdate = false;
meshRoot.add(mesh);
}
const box = new THREE.Box3().setFromObject(meshRoot);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const radius = Math.max(size.x, size.y, size.z) / 2 || 10;
meshRoot.position.sub(center);
camera.position.set(radius * 2.2, radius * 1.6, radius * 2.6);
camera.near = radius / 10;
camera.far = radius * 40;
camera.updateProjectionMatrix();
camera.lookAt(0, 0, 0);
})
.catch(() => {
container.innerHTML = '<p class="empty-hint">Could not load this model.</p>';
});
let isDragging = false;
let lastX = 0, lastY = 0;
renderer.domElement.addEventListener('mousedown', (e) => {
isDragging = true; lastX = e.clientX; lastY = e.clientY;
});
window.addEventListener('mouseup', () => { isDragging = false; });
window.addEventListener('mousemove', (e) => {
if (!isDragging || !meshRoot) return;
const dx = e.clientX - lastX, dy = e.clientY - lastY;
lastX = e.clientX; lastY = e.clientY;
meshRoot.rotation.y += dx * 0.01;
meshRoot.rotation.x += dy * 0.01;
});
renderer.domElement.addEventListener('wheel', (e) => {
e.preventDefault();
camera.position.multiplyScalar(e.deltaY > 0 ? 1.06 : 0.94);
}, { passive: false });
function animate() {
rafId = requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
function dispose() {
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
if (renderer) {
if (meshRoot) disposeObject(meshRoot);
renderer.dispose();
if (renderer.domElement && renderer.domElement.parentNode) {
renderer.domElement.parentNode.removeChild(renderer.domElement);
}
}
renderer = null;
scene = null;
camera = null;
meshRoot = null;
}
window.PacksModelViewer = { render: render, dispose: dispose };
})();