/** * 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} 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 = `
Mode: ${modeLabel} Vertices: ${typeof vertices === 'number' ? vertices.toLocaleString() : vertices} Faces: ${typeof faces === 'number' ? faces.toLocaleString() : faces} Bounds: ${boundsStr} Extents: ${extentsStr} `; // Average edge length (mesh resolution) if (avgEdge !== undefined && avgEdge !== null) { const avgStr = (typeof avgEdge === 'number') ? avgEdge.toLocaleString(undefined, { maximumSignificantDigits: 4 }) : avgEdge; html += ` Avg edge: ${avgStr} `; } // Watertight status if (isWatertight !== undefined) { const watertightText = isWatertight ? 'Yes' : 'No'; const watertightColor = getWatertightColor(isWatertight); html += ` Watertight: ${watertightText} `; } // Mode-specific info if (mode === "texture" || mode === "pbr") { if (visualKind !== undefined) { html += ` Visual Kind: ${visualKind} `; } if (hasTexture !== undefined) { const texColor = hasTexture ? '#c8c' : '#888'; html += ` Textures: ${hasTexture ? 'Yes' : 'No'} `; } if (hasVertexColors !== undefined) { html += ` Vertex Colors: ${hasVertexColors ? 'Yes' : 'No'} `; } } 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) => `${fmt(v)}`; 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 += ` Fields: ${fields} `; } else { html += ` Fields: None `; } } html += '
'; 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 = `
Mode: ${modeLabel}${layoutSuffix} Mesh 1 Mesh 2 Vertices: ${(mesh1.vertices || 'N/A').toLocaleString()} ${(mesh2.vertices || 'N/A').toLocaleString()} Faces: ${(mesh1.faces || 'N/A').toLocaleString()} ${(mesh2.faces || 'N/A').toLocaleString()} `; if (mesh1.faceTypes !== undefined && mesh2.faceTypes !== undefined) { html += ` Face types: ${mesh1.faceTypes || 'n/a'} ${mesh2.faceTypes || 'n/a'} `; } if (mesh1.edgeTypes !== undefined && mesh2.edgeTypes !== undefined) { html += ` Edge types: ${mesh1.edgeTypes || 'n/a'} ${mesh2.edgeTypes || 'n/a'} `; } html += ` Extents: ${extentsStr1} ${extentsStr2} `; // Watertight info if (mesh1.isWatertight !== undefined && mesh2.isWatertight !== undefined) { const color1 = getWatertightColor(mesh1.isWatertight); const color2 = getWatertightColor(mesh2.isWatertight); html += ` Watertight: ${mesh1.isWatertight ? 'Yes' : 'No'} ${mesh2.isWatertight ? 'Yes' : 'No'} `; } // Mode-specific info if (mode === "texture") { if (mesh1.hasTexture !== undefined) { const tex1 = mesh1.hasTexture ? 'Yes' : 'No'; const tex2 = mesh2.hasTexture ? 'Yes' : 'No'; html += ` Textures: ${tex1} ${tex2} `; } } else { // Fields mode if (commonFields && commonFields.length > 0) { html += ` Fields: ${commonFields.length} shared field(s) `; } } html += '
'; return html; }