Files
PozzettiAndrea-ComfyUI-Geom…/javascript/viewer_bvh.html
T
PozzettiAndrea 414f759791 Vendor frontend JS into javascript/; drop the prestartup copy dance
The viewer JS/HTML/bundles are now committed in javascript/ (declared via
pyproject [tool.comfy] web = "javascript") instead of being copied from the
shared comfy-3d-viewers pip package at prestartup. Consequences:

- requirements.txt is now just comfy-env (host env installs nothing else,
  per the never-touch-the-host-env principle)
- prestartup_script.py no longer copies viewers or web_src; it keeps only
  setup_env() and the input-asset copies
- WEB_DIRECTORY attribute removed; the pyproject declaration is the single
  source (served at /extensions/comfyui-geometrypack/, keyed by project name)
- web_src/ deleted; its 6 widgets now live in javascript/js/ as the tracked
  source, edited directly
- viewer_pbr.html hardcoded-fallback URL updated to the new mount name

Iframe URLs auto-detect the mount from import.meta.url, so the folder rename
and mount-name change are transparent at runtime.
2026-08-12 17:37:09 +02:00

278 lines
9.6 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { margin: 0; overflow: hidden; background: #1a1a1a; font-family: Arial, sans-serif; }
#canvas-container { width: 100%; height: 100%; }
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 18px;
z-index: 50;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
font-size: 14px;
font-family: monospace;
z-index: 100;
}
</style>
</head>
<body>
<div id="loading">Loading BVH animation...</div>
<div id="info" style="display: none;"></div>
<div id="canvas-container"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { BVHLoader } from 'three/addons/loaders/BVHLoader.js';
let scene, camera, renderer, controls;
let skeletonHelper = null;
let mixer = null;
let currentAction = null;
let clip = null;
let clock = new THREE.Clock();
let isPlaying = false;
let currentFrame = 0;
let totalFrames = 0;
let frameTime = 0;
let skeleton = null;
let gridHelper, axesHelper;
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(2, 2, 3);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 1, 0);
controls.update();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x222222);
scene.add(gridHelper);
axesHelper = new THREE.AxesHelper(1);
scene.add(axesHelper);
window.addEventListener('resize', onWindowResize);
window.addEventListener('message', handleMessage);
animate();
window.parent.postMessage({ type: 'VIEWER_READY' }, '*');
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function handleMessage(event) {
const data = event.data;
if (data.type === 'loadBVH') {
loadBVHFromString(data.bvhContent, data.bvhInfo);
} else if (data.type === 'play') {
playAnimation();
} else if (data.type === 'pause') {
pauseAnimation();
} else if (data.type === 'setFrame') {
setFrame(data.frame);
} else if (data.type === 'setSpeed') {
if (mixer) mixer.timeScale = data.speed;
}
}
function loadBVHFromString(bvhContent, bvhInfo) {
if (mixer) {
mixer.stopAllAction();
mixer.uncacheRoot(mixer.getRoot());
mixer = null;
}
const toRemove = [];
scene.traverse((child) => {
if (child.isSkeletonHelper || (child.isMesh && child !== gridHelper && child !== axesHelper) || child.type === 'Bone') {
toRemove.push(child);
}
});
toRemove.forEach(child => {
scene.remove(child);
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
else child.material.dispose();
}
});
skeletonHelper = null;
skeleton = null;
const loader = new BVHLoader();
let result;
try {
result = loader.parse(bvhContent);
} catch (error) {
document.getElementById('loading').textContent = 'Error loading BVH: ' + error.message;
return;
}
skeleton = result.skeleton;
clip = result.clip;
scene.add(skeleton.bones[0]);
skeletonHelper = new THREE.SkeletonHelper(skeleton.bones[0]);
skeletonHelper.skeleton = skeleton;
skeletonHelper.material = new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 3, depthTest: false });
scene.add(skeletonHelper);
const jointGeometry = new THREE.SphereGeometry(0.02, 8, 8);
const jointMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, depthTest: false });
skeleton.bones[0].traverse((bone) => {
if (bone.isBone) {
const jointMarker = new THREE.Mesh(jointGeometry, jointMaterial);
bone.add(jointMarker);
}
});
mixer = new THREE.AnimationMixer(skeleton.bones[0]);
currentAction = mixer.clipAction(clip);
currentAction.setEffectiveWeight(1.0);
isPlaying = true;
currentFrame = 0;
totalFrames = Math.floor(clip.duration / (1/30));
if (bvhInfo && bvhInfo.num_frames) totalFrames = bvhInfo.num_frames;
frameTime = clip.duration / totalFrames;
try {
const box = new THREE.Box3().setFromObject(skeleton.bones[0]);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
if (size.lengthSq() < 0.001) {
center.set(0, 1, 0);
size.set(1, 2, 1);
}
const maxDim = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
cameraZ *= 1.5;
camera.position.set(center.x, center.y + size.y/2, center.z + cameraZ);
camera.lookAt(center);
controls.target.copy(center);
controls.update();
} catch (e) {
console.warn("[BVHViewer] Camera framing failed:", e);
}
const infoDiv = document.getElementById('info');
infoDiv.innerHTML = `
<strong>BVH Animation</strong><br>
Bones: ${skeleton.bones.length}<br>
Frames: ${totalFrames}<br>
FPS: ${bvhInfo?.fps || 30}<br>
Duration: ${clip.duration.toFixed(2)}s
`;
infoDiv.style.display = 'block';
document.getElementById('loading').style.display = 'none';
playAnimation();
window.parent.postMessage({ type: 'frameChanged', frame: 0, totalFrames: totalFrames }, '*');
}
function playAnimation() {
if (!currentAction) return;
isPlaying = true;
currentAction.play();
clock.start();
window.parent.postMessage({ type: 'playing' }, '*');
}
function pauseAnimation() {
if (!currentAction) return;
isPlaying = false;
currentAction.paused = true;
window.parent.postMessage({ type: 'paused' }, '*');
}
function setFrame(frame) {
if (!mixer || !currentAction || !clip) return;
currentFrame = Math.max(0, Math.min(frame, totalFrames - 1));
const time = (currentFrame / totalFrames) * clip.duration;
mixer.setTime(time);
currentAction.time = time;
mixer.update(0);
window.parent.postMessage({ type: 'frameChanged', frame: currentFrame, totalFrames: totalFrames }, '*');
}
function animate() {
requestAnimationFrame(animate);
controls.update();
if (mixer && isPlaying) {
const delta = clock.getDelta();
mixer.update(delta);
if (currentAction && clip) {
const progress = currentAction.time / clip.duration;
currentFrame = Math.floor(progress * totalFrames);
if (currentAction.time >= clip.duration - 0.01) {
window.parent.postMessage({ type: 'looped' }, '*');
}
}
}
renderer.render(scene, camera);
}
init();
console.log('[BVH Viewer] Loaded successfully');
</script>
</body>
</html>