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.
734 lines
30 KiB
HTML
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>
|