Files
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

410 lines
16 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GeomPack PBR Viewer</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #1a1a1a;
font-family: monospace;
}
#canvas {
width: 100%;
height: 100%;
display: block;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 14px;
text-align: center;
}
#info {
position: absolute;
bottom: 10px;
left: 10px;
color: #888;
font-size: 11px;
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="loading">Ready to load mesh</div>
<div id="info">PBR Viewer</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 { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
console.log('[GeomPack PBR Viewer] Initializing...');
const canvas = document.getElementById('canvas');
const loading = document.getElementById('loading');
const info = document.getElementById('info');
// Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
// Camera
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 1000);
camera.position.set(2, 2, 2);
// Renderer with PBR settings
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// Critical PBR settings
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.sortObjects = true; // Enable proper transparency sorting
// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.zoomSpeed = 3.0;
// Grid (subtle)
const grid = new THREE.GridHelper(10, 20, 0x333333, 0x222222);
scene.add(grid);
// Send camera state to parent node widget
function sendCameraState() {
window.parent.postMessage({
type: 'WIDGET_UPDATE',
widget: 'camera_state',
value: JSON.stringify({
position: [camera.position.x, camera.position.y, camera.position.z],
focalPoint: [controls.target.x, controls.target.y, controls.target.z],
viewUp: [camera.up.x, camera.up.y, camera.up.z]
})
}, '*');
}
function applyCameraState(cameraState) {
if (!cameraState) return false;
try {
const cs = typeof cameraState === 'string' ? JSON.parse(cameraState) : cameraState;
if (!cs.position || !cs.focalPoint || !cs.viewUp) return false;
camera.position.set(...cs.position);
controls.target.set(...cs.focalPoint);
camera.up.set(...cs.viewUp);
camera.updateProjectionMatrix();
controls.update();
return true;
} catch(e) { return false; }
}
controls.addEventListener('end', sendCameraState);
let _pendingCameraState = null;
// Current mesh
let currentMesh = null;
let envMapLoaded = false;
// Get the extension folder path from the current URL
const getExtensionPath = () => {
const url = window.location.href;
const match = url.match(/\/extensions\/([^/]+)\//);
return match ? `/extensions/${match[1]}` : '/extensions/comfyui-geometrypack';
};
// Load environment map for PBR reflections
const loadEnvironment = () => {
return new Promise((resolve, reject) => {
const extPath = getExtensionPath();
const exrLoader = new EXRLoader();
const exrPath = `${extPath}/assets/studio.exr`;
console.log('[GeomPack PBR Viewer] Loading environment map from:', exrPath);
exrLoader.load(
exrPath,
(texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
envMapLoaded = true;
console.log('[GeomPack PBR Viewer] Environment map Loaded successfully');
resolve(texture);
},
(progress) => {
if (progress.lengthComputable) {
console.log('[GeomPack PBR Viewer] Env map loading:', Math.round(progress.loaded / progress.total * 100) + '%');
}
},
(error) => {
console.error('[GeomPack PBR Viewer] Failed to load EXR environment:', error);
console.warn('[GeomPack PBR Viewer] Using fallback gradient environment');
// Fallback: create a simple gradient environment
createFallbackEnvironment();
resolve(null);
}
);
});
};
// Fallback environment if EXR fails to load
const createFallbackEnvironment = () => {
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
// Create a simple gradient sky
const skyGeo = new THREE.SphereGeometry(500, 32, 15);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x444466) },
bottomColor: { value: new THREE.Color(0x222222) },
offset: { value: 33 },
exponent: { value: 0.6 }
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`,
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeo, skyMat);
// Generate environment map from sky
const envScene = new THREE.Scene();
envScene.add(sky);
const envMap = pmremGenerator.fromScene(envScene).texture;
scene.environment = envMap;
envMapLoaded = true;
pmremGenerator.dispose();
console.log('[GeomPack PBR Viewer] Using fallback gradient environment');
};
// Lighting setup matching standard ComfyUI Load3D viewer
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// Main light
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8);
mainLight.position.set(0, 10, 10);
scene.add(mainLight);
// Back light
const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
backLight.position.set(0, 10, -10);
scene.add(backLight);
// Left fill light
const leftFillLight = new THREE.DirectionalLight(0xffffff, 0.3);
leftFillLight.position.set(-10, 0, 0);
scene.add(leftFillLight);
// Right fill light
const rightFillLight = new THREE.DirectionalLight(0xffffff, 0.3);
rightFillLight.position.set(10, 0, 0);
scene.add(rightFillLight);
// Bottom light
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.2);
bottomLight.position.set(0, -10, 0);
scene.add(bottomLight);
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Load mesh function
async function loadMesh(filepath) {
console.log('[GeomPack PBR Viewer] Loading:', filepath);
loading.textContent = 'Loading mesh...';
loading.style.display = 'block';
// Ensure environment is loaded first
if (!envMapLoaded) {
loading.textContent = 'Loading environment...';
await loadEnvironment();
}
// Remove old mesh
if (currentMesh) {
scene.remove(currentMesh);
currentMesh.traverse((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();
}
}
});
currentMesh = null;
}
loading.textContent = 'Loading mesh...';
const loader = new GLTFLoader();
loader.load(
filepath,
(gltf) => {
currentMesh = gltf.scene;
// Center mesh
const box = new THREE.Box3().setFromObject(currentMesh);
const center = box.getCenter(new THREE.Vector3());
currentMesh.position.sub(center);
// Configure materials for PBR
let meshCount = 0;
let hasTextures = false;
currentMesh.traverse((child) => {
if (child.isMesh) {
meshCount++;
// Enable environment map on all materials
if (child.material) {
const materials = Array.isArray(child.material) ? child.material : [child.material];
materials.forEach(mat => {
if (mat.isMeshStandardMaterial || mat.isMeshPhysicalMaterial) {
mat.envMapIntensity = 1.5;
// Enable transparency support
// Check if baseColorTexture has alpha channel
const hasAlphaMap = mat.map && mat.map.format === THREE.RGBAFormat;
const hasAlphaTest = mat.alphaTest > 0;
const needsTransparency = mat.transparent || mat.opacity < 1.0 || hasAlphaMap;
if (needsTransparency || hasAlphaMap) {
mat.transparent = true;
mat.alphaTest = 0.01; // Discard nearly-transparent pixels
mat.side = THREE.DoubleSide; // Render both sides
mat.depthWrite = true; // Keep depth writing for proper sorting
}
mat.needsUpdate = true;
// Check if has textures
if (mat.map || mat.metalnessMap || mat.roughnessMap) {
hasTextures = true;
}
console.log('[GeomPack PBR Viewer] Material:', {
name: mat.name,
metalness: mat.metalness,
roughness: mat.roughness,
opacity: mat.opacity,
transparent: mat.transparent,
alphaTest: mat.alphaTest,
hasMap: !!mat.map,
mapFormat: mat.map ? mat.map.format : 'none',
hasMetalnessMap: !!mat.metalnessMap,
hasRoughnessMap: !!mat.roughnessMap,
hasAlphaMap: hasAlphaMap
});
}
});
}
}
});
// Add to scene
scene.add(currentMesh);
// Adjust camera (restore saved state or auto-position)
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
camera.near = maxDim * 0.001;
camera.far = maxDim * 100;
camera.updateProjectionMatrix();
if (!applyCameraState(_pendingCameraState)) {
const distance = maxDim * 2.0;
camera.position.set(distance * 0.7, distance * 0.5, distance * 0.7);
camera.lookAt(0, 0, 0);
controls.target.set(0, 0, 0);
controls.update();
}
_pendingCameraState = null;
loading.style.display = 'none';
info.textContent = `PBR Viewer | ${meshCount} mesh(es) | Textures: ${hasTextures ? 'Yes' : 'No'}`;
console.log('[GeomPack PBR Viewer] Mesh Loaded successfully');
},
(progress) => {
if (progress.lengthComputable) {
const percent = (progress.loaded / progress.total * 100).toFixed(0);
loading.textContent = `Loading mesh... ${percent}%`;
}
},
(error) => {
console.error('[GeomPack PBR Viewer] Error loading mesh:', error);
loading.textContent = 'Error loading mesh - check console';
loading.style.color = '#ff6666';
}
);
}
// Listen for messages from parent window
window.addEventListener('message', (event) => {
console.log('[GeomPack PBR Viewer] Received message:', event.data);
if (event.data.type === 'LOAD_MESH') {
_pendingCameraState = event.data.cameraState || null;
loadMesh(event.data.filepath);
}
});
// Pre-load environment
loadEnvironment().then(() => {
console.log('[GeomPack PBR Viewer] Ready');
});
</script>
</body>
</html>