Files
PozzettiAndrea-ComfyUI-Geom…/javascript/viewer_uv.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

734 lines
30 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GeomPack UV Viewer</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #2a2a2a;
font-family: monospace;
}
#container {
display: flex;
width: 100%;
height: 100vh;
}
.view-panel {
flex: 1;
position: relative;
border: 1px solid #444;
}
.view-label {
position: absolute;
top: 5px;
left: 5px;
color: #888;
font-size: 11px;
z-index: 10;
background: rgba(0,0,0,0.5);
padding: 2px 6px;
border-radius: 3px;
}
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;
z-index: 100;
}
#info {
position: absolute;
bottom: 5px;
left: 5px;
color: #aaa;
font-size: 10px;
z-index: 10;
background: rgba(0,0,0,0.7);
padding: 4px 8px;
border-radius: 3px;
}
.highlight-point {
position: absolute;
width: 10px;
height: 10px;
background: #ff4444;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 20;
box-shadow: 0 0 8px #ff4444;
}
</style>
</head>
<body>
<div id="container">
<div class="view-panel" id="mesh-panel">
<div class="view-label">3D Mesh</div>
<canvas id="mesh-canvas"></canvas>
</div>
<div class="view-panel" id="uv-panel">
<div class="view-label">UV Layout</div>
<canvas id="uv-canvas"></canvas>
</div>
</div>
<div id="loading">Ready to load mesh</div>
<div id="info"></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 { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
console.log('[GeomPack UV Viewer] Initializing...');
const loading = document.getElementById('loading');
const infoDiv = document.getElementById('info');
const meshCanvas = document.getElementById('mesh-canvas');
const uvCanvas = document.getElementById('uv-canvas');
const meshPanel = document.getElementById('mesh-panel');
const uvPanel = document.getElementById('uv-panel');
// Configuration
let showChecker = false;
let showWireframe = true;
// 3D Mesh Scene Setup
const meshScene = new THREE.Scene();
meshScene.background = new THREE.Color(0x2a2a2a);
const meshCamera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
meshCamera.position.set(2, 2, 2);
const meshRenderer = new THREE.WebGLRenderer({ canvas: meshCanvas, antialias: true });
meshRenderer.setPixelRatio(window.devicePixelRatio);
const meshControls = new OrbitControls(meshCamera, meshCanvas);
meshControls.enableDamping = true;
meshControls.dampingFactor = 0.05;
meshControls.zoomSpeed = 3.0;
// Lighting for 3D mesh
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
meshScene.add(ambientLight);
const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight1.position.set(5, 10, 7);
meshScene.add(dirLight1);
const dirLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
dirLight2.position.set(-5, -5, -7);
meshScene.add(dirLight2);
// Grid and axes for mesh view
const grid = new THREE.GridHelper(10, 10, 0x444444, 0x222222);
meshScene.add(grid);
const axes = new THREE.AxesHelper(1);
meshScene.add(axes);
// 2D UV Scene Setup
const uvScene = new THREE.Scene();
uvScene.background = new THREE.Color(0x1a1a1a);
const uvCamera = new THREE.OrthographicCamera(0, 1, 1, 0, -1, 1);
uvCamera.position.z = 0.5;
const uvRenderer = new THREE.WebGLRenderer({ canvas: uvCanvas, antialias: true });
uvRenderer.setPixelRatio(window.devicePixelRatio);
// UV grid (0-1 space)
const uvGridHelper = new THREE.GridHelper(1, 10, 0x444444, 0x333333);
uvGridHelper.rotation.x = Math.PI / 2;
uvGridHelper.position.set(0.5, 0.5, -0.1);
uvScene.add(uvGridHelper);
// UV bounds box
const uvBoundsGeom = new THREE.BufferGeometry();
const uvBoundsVerts = new Float32Array([
0, 0, 0, 1, 0, 0,
1, 0, 0, 1, 1, 0,
1, 1, 0, 0, 1, 0,
0, 1, 0, 0, 0, 0
]);
uvBoundsGeom.setAttribute('position', new THREE.BufferAttribute(uvBoundsVerts, 3));
const uvBoundsMat = new THREE.LineBasicMaterial({ color: 0x666666 });
const uvBoundsLine = new THREE.LineSegments(uvBoundsGeom, uvBoundsMat);
uvScene.add(uvBoundsLine);
// Current mesh and UV data
let currentMesh = null;
let currentWireframe = null;
let uvLinesMesh = null;
let uvData = null;
let meshVertices = null;
let meshFaces = null;
// Raycaster for picking
const meshRaycaster = new THREE.Raycaster();
const uvRaycaster = new THREE.Raycaster();
// Highlight markers
let meshHighlightSphere = null; // 3D sphere in scene
let uvHighlight = null;
let highlightedPoint3D = null; // Store 3D world position
let highlightedPointUV = null; // Store UV position
// Create highlight sphere for 3D mesh
const highlightGeom = new THREE.SphereGeometry(1, 16, 16);
const highlightMat = new THREE.MeshBasicMaterial({
color: 0xff4444,
transparent: true,
opacity: 0.9,
depthTest: true,
depthWrite: true
});
// Create crosshair lines for highlighting (unused - kept for reference)
// function createCrosshair() {
// const lineGeom = new THREE.BufferGeometry();
// const positions = new Float32Array([
// -2, 0, 0, 2, 0, 0, // X axis (red)
// 0, -2, 0, 0, 2, 0, // Y axis (green)
// 0, 0, -2, 0, 0, 2 // Z axis (blue)
// ]);
// lineGeom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
//
// // Create different colors for each axis
// const colors = new Float32Array([
// 1, 0, 0, 1, 0, 0, // X axis - red
// 0, 1, 0, 0, 1, 0, // Y axis - green
// 0, 0, 1, 0, 0, 1 // Z axis - blue
// ]);
// lineGeom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
//
// const lineMat = new THREE.LineBasicMaterial({
// vertexColors: true,
// linewidth: 2,
// depthTest: true,
// depthWrite: true,
// transparent: true,
// opacity: 0.8
// });
//
// return new THREE.LineSegments(lineGeom, lineMat);
// }
// Update highlight positions (call in animation loop)
function updateHighlightPositions() {
// Sphere position is already set, no need to update
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
meshControls.update();
updateHighlightPositions();
meshRenderer.render(meshScene, meshCamera);
uvRenderer.render(uvScene, uvCamera);
}
animate();
// Handle window resize
function updateSizes() {
const meshRect = meshPanel.getBoundingClientRect();
const uvRect = uvPanel.getBoundingClientRect();
meshCamera.aspect = meshRect.width / meshRect.height;
meshCamera.updateProjectionMatrix();
meshRenderer.setSize(meshRect.width, meshRect.height);
// Update UV camera to maintain aspect ratio
const uvAspect = uvRect.width / uvRect.height;
if (uvAspect > 1) {
uvCamera.left = -0.1;
uvCamera.right = 1.1;
uvCamera.top = 0.6 + 0.5 / uvAspect;
uvCamera.bottom = 0.4 - 0.5 / uvAspect;
} else {
uvCamera.left = 0.5 - 0.5 * uvAspect - 0.1;
uvCamera.right = 0.5 + 0.5 * uvAspect + 0.1;
uvCamera.top = 1.1;
uvCamera.bottom = -0.1;
}
uvCamera.updateProjectionMatrix();
uvRenderer.setSize(uvRect.width, uvRect.height);
}
window.addEventListener('resize', updateSizes);
setTimeout(updateSizes, 100);
// Create checker texture
function createCheckerTexture(size = 512, squares = 16) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
const squareSize = size / squares;
for (let i = 0; i < squares; i++) {
for (let j = 0; j < squares; j++) {
ctx.fillStyle = (i + j) % 2 === 0 ? '#888888' : '#444444';
ctx.fillRect(i * squareSize, j * squareSize, squareSize, squareSize);
}
}
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
return texture;
}
// Load mesh
function loadMesh(filepath, uvDataPath) {
console.log('[GeomPack UV Viewer] Loading mesh:', filepath);
loading.textContent = 'Loading mesh...';
loading.style.display = 'block';
// Remove old mesh
if (currentMesh) {
meshScene.remove(currentMesh);
currentMesh = null;
}
if (currentWireframe) {
meshScene.remove(currentWireframe);
currentWireframe = null;
}
if (uvLinesMesh) {
uvScene.remove(uvLinesMesh);
uvLinesMesh = null;
}
const isGLB = filepath.includes('.glb') || filepath.includes('.gltf');
const loader = isGLB ? new GLTFLoader() : new OBJLoader();
loader.load(
filepath,
(result) => {
currentMesh = isGLB ? result.scene : result;
// Center mesh
const box = new THREE.Box3().setFromObject(currentMesh);
const center = box.getCenter(new THREE.Vector3());
currentMesh.position.sub(center);
// Store mesh geometry for picking
currentMesh.traverse((child) => {
if (child.isMesh) {
meshVertices = child.geometry.attributes.position.array;
if (child.geometry.index) {
meshFaces = child.geometry.index.array;
}
// Apply material
if (showChecker) {
child.material = new THREE.MeshStandardMaterial({
map: createCheckerTexture(),
metalness: 0.1,
roughness: 0.9,
side: THREE.DoubleSide
});
} else {
child.material = new THREE.MeshStandardMaterial({
color: 0x88aacc,
metalness: 0.3,
roughness: 0.7,
side: THREE.DoubleSide
});
}
// Add wireframe overlay as child of mesh (inherits transforms)
if (showWireframe) {
const wireframeGeom = new THREE.WireframeGeometry(child.geometry);
const wireframeMat = new THREE.LineBasicMaterial({
color: 0x000000,
opacity: 0.3,
transparent: true
});
currentWireframe = new THREE.LineSegments(wireframeGeom, wireframeMat);
// Add as child of the mesh child so it inherits position/rotation
child.add(currentWireframe);
}
}
});
meshScene.add(currentMesh);
// Adjust camera
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const distance = maxDim * 2.5;
meshCamera.position.set(distance, distance, distance);
meshCamera.lookAt(0, 0, 0);
meshControls.target.set(0, 0, 0);
meshControls.update();
loading.textContent = 'Loading UV data...';
// Load UV data
if (uvDataPath) {
fetch(uvDataPath)
.then(response => response.json())
.then(data => {
uvData = data;
createUVLayout(data);
loading.style.display = 'none';
updateInfo();
console.log('[GeomPack UV Viewer] UV data loaded:', data.uvs.length, 'UVs');
})
.catch(err => {
console.error('[GeomPack UV Viewer] Error loading UV data:', err);
loading.textContent = 'No UV data available';
setTimeout(() => { loading.style.display = 'none'; }, 2000);
});
} else {
loading.textContent = 'No UV data';
setTimeout(() => { loading.style.display = 'none'; }, 2000);
}
console.log('[GeomPack UV 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 UV Viewer] Error loading mesh:', error);
loading.textContent = 'Error loading mesh';
loading.style.color = '#ff6666';
}
);
}
// Create UV layout wireframe
function createUVLayout(data) {
const uvs = data.uvs;
const faces = data.faces;
// Create line segments for UV triangles
const positions = [];
for (const face of faces) {
const uv0 = uvs[face[0]];
const uv1 = uvs[face[1]];
const uv2 = uvs[face[2]];
// Edge 0-1
positions.push(uv0[0], uv0[1], 0);
positions.push(uv1[0], uv1[1], 0);
// Edge 1-2
positions.push(uv1[0], uv1[1], 0);
positions.push(uv2[0], uv2[1], 0);
// Edge 2-0
positions.push(uv2[0], uv2[1], 0);
positions.push(uv0[0], uv0[1], 0);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const material = new THREE.LineBasicMaterial({
color: 0x44aaff,
opacity: 0.8,
transparent: true
});
uvLinesMesh = new THREE.LineSegments(geometry, material);
uvScene.add(uvLinesMesh);
}
// Update info display
function updateInfo() {
let info = '';
if (uvData) {
info = `UVs: ${uvData.uvs.length} | Faces: ${uvData.faces.length}`;
}
infoDiv.textContent = info;
}
// Highlight corresponding points on click
function highlightPoint(point3D, pointUV) {
// Remove old highlights
if (meshHighlightSphere) {
meshScene.remove(meshHighlightSphere);
meshHighlightSphere = null;
}
if (uvHighlight) {
uvPanel.removeChild(uvHighlight);
uvHighlight = null;
}
// Store positions for continuous updates
highlightedPoint3D = point3D ? point3D.clone() : null;
highlightedPointUV = pointUV ? pointUV.clone() : null;
// Create 3D mesh highlight sphere
if (point3D) {
meshHighlightSphere = new THREE.Mesh(highlightGeom, highlightMat);
meshHighlightSphere.position.copy(point3D);
// Scale sphere based on mesh size
if (currentMesh) {
const box = new THREE.Box3().setFromObject(currentMesh);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = maxDim * 0.03; // 3% of mesh size (slightly larger for better visibility)
meshHighlightSphere.scale.setScalar(scale);
}
// Ensure sphere always renders on top
meshHighlightSphere.renderOrder = 999;
meshScene.add(meshHighlightSphere);
console.log('[GeomPack UV Viewer] Sphere placed at:', point3D.x, point3D.y, point3D.z, 'scale:', meshHighlightSphere.scale.x);
}
// Create UV highlight
if (pointUV) {
const uvRect = uvCanvas.getBoundingClientRect();
// Convert UV coords to screen coords
const ndcX = (pointUV.x - uvCamera.left) / (uvCamera.right - uvCamera.left);
const ndcY = (pointUV.y - uvCamera.bottom) / (uvCamera.top - uvCamera.bottom);
const x = ndcX * uvRect.width;
const y = (1 - ndcY) * uvRect.height;
uvHighlight = document.createElement('div');
uvHighlight.className = 'highlight-point';
uvHighlight.style.left = x + 'px';
uvHighlight.style.top = y + 'px';
uvPanel.appendChild(uvHighlight);
}
}
// Mesh canvas click detection (using mousedown/mouseup to avoid OrbitControls interference)
let meshMouseDownPos = null;
let meshMouseDownTime = 0;
meshCanvas.addEventListener('mousedown', (event) => {
meshMouseDownPos = new THREE.Vector2(event.clientX, event.clientY);
meshMouseDownTime = Date.now();
console.log('[GeomPack UV Viewer DEBUG] Mesh mousedown at:', event.clientX, event.clientY);
});
meshCanvas.addEventListener('mouseup', (event) => {
console.log('[GeomPack UV Viewer DEBUG] Mesh mouseup - currentMesh:', !!currentMesh, 'uvData:', !!uvData, 'mouseDownPos:', !!meshMouseDownPos);
if (!currentMesh || !uvData || !meshMouseDownPos) return;
const mouseUpPos = new THREE.Vector2(event.clientX, event.clientY);
const dragDistance = meshMouseDownPos.distanceTo(mouseUpPos);
const dragTime = Date.now() - meshMouseDownTime;
console.log('[GeomPack UV Viewer DEBUG] Drag distance:', dragDistance.toFixed(2), 'px, Time:', dragTime, 'ms');
// Only treat as click if mouse moved less than 5 pixels and took less than 300ms
if (dragDistance < 5 && dragTime < 300) {
console.log('[GeomPack UV Viewer DEBUG] Click threshold met, raycasting...');
const rect = meshCanvas.getBoundingClientRect();
const mouse = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1
);
meshRaycaster.setFromCamera(mouse, meshCamera);
const intersects = meshRaycaster.intersectObject(currentMesh, true);
console.log('[GeomPack UV Viewer DEBUG] Intersections found:', intersects.length);
if (intersects.length > 0) {
const hit = intersects[0];
const hitMesh = hit.object;
const faceIndex = hit.faceIndex;
console.log('[GeomPack UV Viewer DEBUG] Hit face index:', faceIndex);
// Get the 3 vertex positions from the hit face
const geometry = hitMesh.geometry;
const position = geometry.attributes.position;
const index = geometry.index;
if (index && faceIndex * 3 + 2 < index.count) {
const i0 = index.array[faceIndex * 3];
const i1 = index.array[faceIndex * 3 + 1];
const i2 = index.array[faceIndex * 3 + 2];
// Get vertex positions in local space
const v0 = new THREE.Vector3(position.getX(i0), position.getY(i0), position.getZ(i0));
const v1 = new THREE.Vector3(position.getX(i1), position.getY(i1), position.getZ(i1));
const v2 = new THREE.Vector3(position.getX(i2), position.getY(i2), position.getZ(i2));
console.log('[GeomPack UV Viewer DEBUG] Hit face vertices:', v0.toArray(), v1.toArray(), v2.toArray());
// Find matching face in uvData by comparing vertex positions
let matchedFaceIdx = -1;
const tolerance = 0.0001;
currentMesh.traverse((child) => {
if (child.isMesh && child.geometry && matchedFaceIdx === -1) {
const childPos = child.geometry.attributes.position;
// Search through all faces to find matching vertices
for (let fIdx = 0; fIdx < uvData.faces.length; fIdx++) {
const face = uvData.faces[fIdx];
const fv0 = new THREE.Vector3(childPos.getX(face[0]), childPos.getY(face[0]), childPos.getZ(face[0]));
const fv1 = new THREE.Vector3(childPos.getX(face[1]), childPos.getY(face[1]), childPos.getZ(face[1]));
const fv2 = new THREE.Vector3(childPos.getX(face[2]), childPos.getY(face[2]), childPos.getZ(face[2]));
// Check if vertices match (in any order)
if ((v0.distanceTo(fv0) < tolerance && v1.distanceTo(fv1) < tolerance && v2.distanceTo(fv2) < tolerance) ||
(v0.distanceTo(fv0) < tolerance && v1.distanceTo(fv2) < tolerance && v2.distanceTo(fv1) < tolerance) ||
(v0.distanceTo(fv1) < tolerance && v1.distanceTo(fv0) < tolerance && v2.distanceTo(fv2) < tolerance) ||
(v0.distanceTo(fv1) < tolerance && v1.distanceTo(fv2) < tolerance && v2.distanceTo(fv0) < tolerance) ||
(v0.distanceTo(fv2) < tolerance && v1.distanceTo(fv0) < tolerance && v2.distanceTo(fv1) < tolerance) ||
(v0.distanceTo(fv2) < tolerance && v1.distanceTo(fv1) < tolerance && v2.distanceTo(fv0) < tolerance)) {
matchedFaceIdx = fIdx;
break;
}
}
}
});
if (matchedFaceIdx >= 0) {
const face = uvData.faces[matchedFaceIdx];
console.log('[GeomPack UV Viewer DEBUG] Matched to uvData face index:', matchedFaceIdx);
// Interpolate UV coordinates
const uv0 = uvData.uvs[face[0]];
const uv1 = uvData.uvs[face[1]];
const uv2 = uvData.uvs[face[2]];
// Simple center of triangle for now
const uvPoint = new THREE.Vector2(
(uv0[0] + uv1[0] + uv2[0]) / 3,
(uv0[1] + uv1[1] + uv2[1]) / 3
);
highlightPoint(hit.point, uvPoint);
console.log('[GeomPack UV Viewer] Clicked face:', matchedFaceIdx, 'UV:', uvPoint);
} else {
console.log('[GeomPack UV Viewer DEBUG] No matching face found in uvData');
}
} else {
console.log('[GeomPack UV Viewer DEBUG] Invalid face index or geometry');
}
} else {
console.log('[GeomPack UV Viewer DEBUG] No intersections - missed the mesh');
}
} else {
console.log('[GeomPack UV Viewer DEBUG] Click threshold NOT met (drag or too slow)');
}
meshMouseDownPos = null;
});
// UV canvas click handler
uvCanvas.addEventListener('click', (event) => {
if (!currentMesh || !uvData) return;
const rect = uvCanvas.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
const y = (event.clientY - rect.top) / rect.height;
// Convert screen coords to UV space
const uvX = uvCamera.left + x * (uvCamera.right - uvCamera.left);
const uvY = uvCamera.top - y * (uvCamera.top - uvCamera.bottom);
// Find nearest face in UV space
let nearestFace = -1;
let nearestDist = Infinity;
for (let i = 0; i < uvData.faces.length; i++) {
const face = uvData.faces[i];
const uv0 = uvData.uvs[face[0]];
const uv1 = uvData.uvs[face[1]];
const uv2 = uvData.uvs[face[2]];
// Check if point is inside triangle (simplified - just use center distance)
const centerX = (uv0[0] + uv1[0] + uv2[0]) / 3;
const centerY = (uv0[1] + uv1[1] + uv2[1]) / 3;
const dist = Math.sqrt((uvX - centerX) ** 2 + (uvY - centerY) ** 2);
if (dist < nearestDist) {
nearestDist = dist;
nearestFace = i;
}
}
if (nearestFace >= 0 && nearestDist < 0.1) {
// Get 3D position of face center
const face = uvData.faces[nearestFace];
const uv0 = uvData.uvs[face[0]];
const uv1 = uvData.uvs[face[1]];
const uv2 = uvData.uvs[face[2]];
const uvPoint = new THREE.Vector2(
(uv0[0] + uv1[0] + uv2[0]) / 3,
(uv0[1] + uv1[1] + uv2[1]) / 3
);
// Get 3D face center from mesh
let point3D = null;
currentMesh.traverse((child) => {
if (child.isMesh && child.geometry) {
const pos = child.geometry.attributes.position;
const idx = child.geometry.index;
if (idx && nearestFace * 3 + 2 < idx.count) {
const i0 = idx.array[nearestFace * 3];
const i1 = idx.array[nearestFace * 3 + 1];
const i2 = idx.array[nearestFace * 3 + 2];
const v0 = new THREE.Vector3(pos.getX(i0), pos.getY(i0), pos.getZ(i0));
const v1 = new THREE.Vector3(pos.getX(i1), pos.getY(i1), pos.getZ(i1));
const v2 = new THREE.Vector3(pos.getX(i2), pos.getY(i2), pos.getZ(i2));
point3D = new THREE.Vector3()
.addVectors(v0, v1)
.add(v2)
.divideScalar(3);
// Transform from local object space to world space
child.localToWorld(point3D);
}
}
});
highlightPoint(point3D, uvPoint);
console.log('[GeomPack UV Viewer] Clicked UV:', uvX.toFixed(3), uvY.toFixed(3), 'Face:', nearestFace);
}
});
// Listen for messages from parent window
window.addEventListener('message', (event) => {
console.log('[GeomPack UV Viewer] Received message:', event.data);
if (event.data.type === 'LOAD_MESH_UV') {
const { meshPath, uvDataPath, checker, wireframe } = event.data;
showChecker = checker || false;
showWireframe = wireframe !== false;
loadMesh(meshPath, uvDataPath);
}
});
console.log('[GeomPack UV Viewer] Ready');
</script>
</body>
</html>