Files
PozzettiAndrea 7947ff3f23 Vendor viewer JS, drop comfy-3d-viewers
Vendor the viewer JavaScript into javascript/ and drop the
comfy-3d-viewers dependency. The viewers were previously copied into web/ at
every startup by prestartup_script.py, which made the served frontend depend on
a second package resolving at runtime.

Bundles are vendored as .mjs so ComfyUI's **/*.js auto-import glob skips them:
they are iframe-only and must never enter the shared main browser realm. Only
the widget entry points stay .js.

Also replace comfy_env.copy_files with a local helper. Its two extra features
(frame-derived source dir, silent no-op on a missing source) were unused here,
and the asset seeding now resolves the input directory through
folder_paths.get_input_directory() so it follows ComfyUI Desktop and
--input-directory instead of writing into the code tree.

comfy-test.toml moves to the current schema: [test.workflows] cuda (there is no
"gpu" key -- the accelerator is named by backend, and the old spelling parsed to
an empty list, so CUDA runners silently ran the full unfiltered set), an
explicit [test.platforms] platforms allowlist (per-platform booleans are no
longer supported), and the javascript level.

DisplayName becomes "DepthAnythingV3": the namespace enforced on
registerExtension names is derived from it, and the space in "Depth Anything
V3" made that prefix invalid.

Delete the 21 loose js/viewer/*.js modules -- they are already inside
viewer-bundle-vtk.mjs, so shipping them loose leaked the whole viewer into the
shared realm.
2026-08-22 19:44:03 +02:00

291 lines
10 KiB
JavaScript

/**
* Formatting utilities for mesh metadata display
*/
/**
* Format bounds array as string
* @param {number[]} boundsMin - [x, y, z] minimum bounds
* @param {number[]} boundsMax - [x, y, z] maximum bounds
* @param {number} decimals - Number of decimal places (default: 2)
* @returns {string} Formatted bounds string or 'N/A'
*/
export function formatBounds(boundsMin, boundsMax, decimals = 2) {
if (!boundsMin || !boundsMax || boundsMin.length !== 3 || boundsMax.length !== 3) {
return 'N/A';
}
const min = boundsMin.map(v => v.toFixed(decimals)).join(', ');
const max = boundsMax.map(v => v.toFixed(decimals)).join(', ');
return `[${min}] to [${max}]`;
}
/**
* Format extents array as string
* @param {number[]} extents - [x, y, z] extents
* @param {number} decimals - Number of decimal places (default: 2)
* @returns {string} Formatted extents string or 'N/A'
*/
export function formatExtents(extents, decimals = 2) {
if (!extents || extents.length !== 3) {
return 'N/A';
}
return extents.map(v => v.toFixed(decimals)).join(' x ');
}
/**
* Format a {typeName: count} map as a sorted, human-readable string, e.g.
* "3 Planes, 2 Cylinders, 1 BSpline".
* @param {Object<string, number>} counts
* @returns {string} Formatted string (empty string if counts is empty/missing)
*/
export function formatTypeCounts(counts) {
if (!counts) return '';
return Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.map(([name, count]) => `${count} ${name}${count === 1 ? '' : 's'}`)
.join(', ');
}
/**
* Get color for mode label
* @param {string} mode - Viewer mode (fields, texture, pbr)
* @returns {string} CSS color
*/
export function getModeColor(mode) {
switch (mode) {
case "texture (PBR)":
case "pbr":
return '#fc6';
case "texture":
return '#c8c';
default:
return '#6cc';
}
}
/**
* Get color for watertight status
* @param {boolean} isWatertight
* @returns {string} CSS color
*/
export function getWatertightColor(isWatertight) {
return isWatertight ? '#6c6' : '#c66';
}
/**
* Build mesh info HTML for single mesh viewer
* @param {Object} data - Mesh metadata
* @returns {string} HTML string
*/
export function buildMeshInfoHTML(data) {
const {
mode = "fields",
vertices = 'N/A',
faces = 'N/A',
boundsMin = [],
boundsMax = [],
extents = [],
avgEdge,
isWatertight,
fieldNames = [],
fieldRanges = [],
hasTexture,
hasVertexColors,
visualKind
} = data;
const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1);
const modeColor = getModeColor(mode);
const boundsStr = formatBounds(boundsMin, boundsMax);
const extentsStr = formatExtents(extents);
let html = `
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
<span style="color: #888;">Mode:</span>
<span style="color: ${modeColor}; font-weight: bold;">${modeLabel}</span>
<span style="color: #888;">Vertices:</span>
<span>${typeof vertices === 'number' ? vertices.toLocaleString() : vertices}</span>
<span style="color: #888;">Faces:</span>
<span>${typeof faces === 'number' ? faces.toLocaleString() : faces}</span>
<span style="color: #888;">Bounds:</span>
<span style="font-size: 9px;">${boundsStr}</span>
<span style="color: #888;">Extents:</span>
<span>${extentsStr}</span>
`;
// Average edge length (mesh resolution)
if (avgEdge !== undefined && avgEdge !== null) {
const avgStr = (typeof avgEdge === 'number')
? avgEdge.toLocaleString(undefined, { maximumSignificantDigits: 4 })
: avgEdge;
html += `
<span style="color: #888;">Avg edge:</span>
<span>${avgStr}</span>
`;
}
// Watertight status
if (isWatertight !== undefined) {
const watertightText = isWatertight ? 'Yes' : 'No';
const watertightColor = getWatertightColor(isWatertight);
html += `
<span style="color: #888;">Watertight:</span>
<span style="color: ${watertightColor};">${watertightText}</span>
`;
}
// Mode-specific info
if (mode === "texture" || mode === "pbr") {
if (visualKind !== undefined) {
html += `
<span style="color: #888;">Visual Kind:</span>
<span>${visualKind}</span>
`;
}
if (hasTexture !== undefined) {
const texColor = hasTexture ? '#c8c' : '#888';
html += `
<span style="color: #888;">Textures:</span>
<span style="color: ${texColor};">${hasTexture ? 'Yes' : 'No'}</span>
`;
}
if (hasVertexColors !== undefined) {
html += `
<span style="color: #888;">Vertex Colors:</span>
<span>${hasVertexColors ? 'Yes' : 'No'}</span>
`;
}
} else {
// Fields mode — annotate each field with its [min, max] value range
if (fieldNames && fieldNames.length > 0) {
const fmt = (n) => {
if (n == null || !isFinite(n)) return '?';
if (Number.isInteger(n) && Math.abs(n) < 1e7) return String(n);
return Number(Number(n).toPrecision(4)).toString();
};
const ext = (i, which, v) =>
`<span class="field-extreme" data-fidx="${i}" data-which="${which}" ` +
`style="cursor:pointer;text-decoration:underline dotted;" ` +
`title="focus camera on ${which}">${fmt(v)}</span>`;
const fields = fieldNames.map((name, i) => {
const r = fieldRanges && fieldRanges[i];
return (r && r.length === 2)
? `${name} [${ext(i, 'min', r[0])}, ${ext(i, 'max', r[1])}]`
: name;
}).join(', ');
html += `
<span style="color: #888;">Fields:</span>
<span style="font-size: 9px; color: #6cc;">${fields}</span>
`;
} else {
html += `
<span style="color: #888;">Fields:</span>
<span style="color: #888;">None</span>
`;
}
}
html += '</div>';
return html;
}
/**
* Build dual mesh info HTML for side-by-side comparison
* @param {Object} data - Dual mesh metadata
* @returns {string} HTML string
*/
export function buildDualMeshInfoHTML(data) {
const {
mode = "fields",
layout = "side_by_side",
mesh1 = {},
mesh2 = {},
commonFields = []
} = data;
const modeLabel = mode.charAt(0).toUpperCase() + mode.slice(1);
const modeColor = getModeColor(mode);
const extentsStr1 = formatExtents(mesh1.extents);
const extentsStr2 = formatExtents(mesh2.extents);
const layoutSuffix = layout === "overlay" ? " (Overlay)" : "";
let html = `
<div style="display: grid; grid-template-columns: auto 1fr 1fr; gap: 2px 12px;">
<span style="color: #888;">Mode:</span>
<span colspan="2" style="grid-column: 2 / 4; color: ${modeColor}; font-weight: bold;">${modeLabel}${layoutSuffix}</span>
<span style="color: #888;"></span>
<span style="color: #999; font-weight: bold; border-bottom: 1px solid #333;">Mesh 1</span>
<span style="color: #999; font-weight: bold; border-bottom: 1px solid #333;">Mesh 2</span>
<span style="color: #888;">Vertices:</span>
<span>${(mesh1.vertices || 'N/A').toLocaleString()}</span>
<span>${(mesh2.vertices || 'N/A').toLocaleString()}</span>
<span style="color: #888;">Faces:</span>
<span>${(mesh1.faces || 'N/A').toLocaleString()}</span>
<span>${(mesh2.faces || 'N/A').toLocaleString()}</span>
`;
if (mesh1.faceTypes !== undefined && mesh2.faceTypes !== undefined) {
html += `
<span style="color: #888;">Face types:</span>
<span style="font-size: 9px;">${mesh1.faceTypes || 'n/a'}</span>
<span style="font-size: 9px;">${mesh2.faceTypes || 'n/a'}</span>
`;
}
if (mesh1.edgeTypes !== undefined && mesh2.edgeTypes !== undefined) {
html += `
<span style="color: #888;">Edge types:</span>
<span style="font-size: 9px;">${mesh1.edgeTypes || 'n/a'}</span>
<span style="font-size: 9px;">${mesh2.edgeTypes || 'n/a'}</span>
`;
}
html += `
<span style="color: #888;">Extents:</span>
<span style="font-size: 9px;">${extentsStr1}</span>
<span style="font-size: 9px;">${extentsStr2}</span>
`;
// Watertight info
if (mesh1.isWatertight !== undefined && mesh2.isWatertight !== undefined) {
const color1 = getWatertightColor(mesh1.isWatertight);
const color2 = getWatertightColor(mesh2.isWatertight);
html += `
<span style="color: #888;">Watertight:</span>
<span style="color: ${color1};">${mesh1.isWatertight ? 'Yes' : 'No'}</span>
<span style="color: ${color2};">${mesh2.isWatertight ? 'Yes' : 'No'}</span>
`;
}
// Mode-specific info
if (mode === "texture") {
if (mesh1.hasTexture !== undefined) {
const tex1 = mesh1.hasTexture ? 'Yes' : 'No';
const tex2 = mesh2.hasTexture ? 'Yes' : 'No';
html += `
<span style="color: #888;">Textures:</span>
<span style="color: ${mesh1.hasTexture ? '#c8c' : '#888'};">${tex1}</span>
<span style="color: ${mesh2.hasTexture ? '#c8c' : '#888'};">${tex2}</span>
`;
}
} else {
// Fields mode
if (commonFields && commonFields.length > 0) {
html += `
<span style="color: #888;">Fields:</span>
<span colspan="2" style="grid-column: 2 / 4; color: #9c9;">${commonFields.length} shared field(s)</span>
`;
}
}
html += '</div>';
return html;
}