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.
353 lines
12 KiB
JavaScript
353 lines
12 KiB
JavaScript
/**
|
|
* FieldVisualization - Scalar field coloring for VTK viewers
|
|
*
|
|
* Handles scalar field visualization including:
|
|
* - Field detection (point/cell data)
|
|
* - Colormap application
|
|
* - Range calculation (auto/manual, single/synchronized)
|
|
*/
|
|
|
|
/**
|
|
* Popular colormaps to show at top of selector
|
|
*/
|
|
const POPULAR_COLORMAPS = [
|
|
'erdc_rainbow_bright',
|
|
'Cool to Warm',
|
|
'Viridis (matplotlib)',
|
|
'Plasma (matplotlib)',
|
|
'Inferno (matplotlib)',
|
|
'Magma (matplotlib)',
|
|
'jet',
|
|
'rainbow'
|
|
];
|
|
|
|
export class FieldVisualization {
|
|
/**
|
|
* Create a FieldVisualization instance
|
|
* @param {Object} vtk - VTK.js global object
|
|
* @param {Object} renderWindow - VTK render window for triggering renders
|
|
*/
|
|
constructor(vtk, renderWindow) {
|
|
this.vtk = vtk;
|
|
this.renderWindow = renderWindow;
|
|
|
|
// VTK classes
|
|
this.vtkColorTransferFunction = vtk.Rendering.Core.vtkColorTransferFunction;
|
|
this.vtkColorMaps = vtk.Rendering.Core.vtkColorTransferFunction.vtkColorMaps;
|
|
|
|
// State
|
|
this.currentField = null;
|
|
this.currentColormap = 'erdc_rainbow_bright';
|
|
this.autoRange = true;
|
|
this.manualRange = [0, 1];
|
|
}
|
|
|
|
/**
|
|
* Extract field information from polydata
|
|
* @param {Object} polyData - VTK polydata object
|
|
* @returns {Object} Field information with pointFields and cellFields arrays
|
|
*/
|
|
extractFields(polyData) {
|
|
if (!polyData) {
|
|
return { pointFields: [], cellFields: [], allFields: [] };
|
|
}
|
|
|
|
// Fields to hide from dropdown (VTK internal arrays that aren't useful to visualize)
|
|
const HIDDEN_FIELDS = ['Attribute', 'vtkOriginalPointIds', 'vtkOriginalCellIds'];
|
|
|
|
const pointData = polyData.getPointData();
|
|
const cellData = polyData.getCellData();
|
|
|
|
const pointFields = [];
|
|
const cellFields = [];
|
|
|
|
// Extract point (vertex) data arrays
|
|
for (let i = 0; i < pointData.getNumberOfArrays(); i++) {
|
|
const array = pointData.getArray(i);
|
|
const name = array.getName();
|
|
// Skip hidden/internal fields
|
|
if (HIDDEN_FIELDS.includes(name)) continue;
|
|
const range = array.getRange();
|
|
pointFields.push({
|
|
name,
|
|
displayName: `${name} (vertex)`,
|
|
type: 'point',
|
|
fullName: `point:${name}`,
|
|
numComponents: array.getNumberOfComponents(),
|
|
range: [range[0], range[1]]
|
|
});
|
|
}
|
|
|
|
// Extract cell (face) data arrays
|
|
for (let i = 0; i < cellData.getNumberOfArrays(); i++) {
|
|
const array = cellData.getArray(i);
|
|
const name = array.getName();
|
|
// Skip hidden/internal fields
|
|
if (HIDDEN_FIELDS.includes(name)) continue;
|
|
const range = array.getRange();
|
|
cellFields.push({
|
|
name,
|
|
displayName: `${name} (face)`,
|
|
type: 'cell',
|
|
fullName: `cell:${name}`,
|
|
numComponents: array.getNumberOfComponents(),
|
|
range: [range[0], range[1]]
|
|
});
|
|
}
|
|
|
|
const allFields = [...pointFields, ...cellFields];
|
|
|
|
return {
|
|
pointFields,
|
|
cellFields,
|
|
allFields,
|
|
hasFields: allFields.length > 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply a scalar field to a mapper
|
|
* @param {Object} mapper - VTK mapper
|
|
* @param {Object} polyData - VTK polydata
|
|
* @param {string} fieldName - Field name (e.g., "point:temperature" or "cell:pressure")
|
|
* @param {Object} options - Options
|
|
* @param {number[]} options.range - Manual range [min, max] (overrides auto)
|
|
* @param {string} options.colormap - Colormap name
|
|
* @param {boolean} options.render - Whether to trigger render (default: true)
|
|
*/
|
|
applyField(mapper, polyData, fieldName, options = {}) {
|
|
const {
|
|
range = null,
|
|
colormap = this.currentColormap,
|
|
render = true
|
|
} = options;
|
|
|
|
if (!fieldName || fieldName === 'none' || !polyData) {
|
|
this.disableScalarVisualization(mapper, render);
|
|
return;
|
|
}
|
|
|
|
// Parse field name
|
|
const isCellData = fieldName.startsWith('cell:');
|
|
const actualFieldName = isCellData
|
|
? fieldName.substring(5)
|
|
: fieldName.startsWith('point:')
|
|
? fieldName.substring(6)
|
|
: fieldName;
|
|
|
|
// Get data array
|
|
const data = isCellData ? polyData.getCellData() : polyData.getPointData();
|
|
const scalarArray = data.getArrayByName(actualFieldName);
|
|
|
|
if (!scalarArray) {
|
|
console.warn(`[FieldVisualization] Field "${actualFieldName}" not found`);
|
|
this.disableScalarVisualization(mapper, render);
|
|
return;
|
|
}
|
|
|
|
// Get range
|
|
const dataRange = scalarArray.getRange();
|
|
const [minVal, maxVal] = range || (this.autoRange ? dataRange : this.manualRange);
|
|
|
|
// Check for degenerate range
|
|
const epsilon = 1e-10;
|
|
if (Math.abs(maxVal - minVal) < epsilon && !range) {
|
|
console.warn(`[FieldVisualization] Field "${actualFieldName}" has zero-width range`);
|
|
this.disableScalarVisualization(mapper, render);
|
|
return;
|
|
}
|
|
|
|
// Set active scalars
|
|
data.setActiveScalars(actualFieldName);
|
|
|
|
// Configure mapper
|
|
mapper.setScalarVisibility(true);
|
|
if (isCellData) {
|
|
mapper.setScalarModeToUseCellData();
|
|
} else {
|
|
mapper.setScalarModeToUsePointData();
|
|
}
|
|
mapper.setScalarRange(minVal, maxVal);
|
|
|
|
// Apply colormap
|
|
this.applyColormap(mapper, colormap, minVal, maxVal);
|
|
|
|
// Update state
|
|
this.currentField = fieldName;
|
|
this.currentColormap = colormap;
|
|
|
|
// Update mapper to apply changes
|
|
mapper.update();
|
|
|
|
if (render) {
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
console.log(`[FieldVisualization] Applied field "${actualFieldName}" with range [${minVal.toFixed(3)}, ${maxVal.toFixed(3)}]`);
|
|
}
|
|
|
|
/**
|
|
* Apply a colormap to a mapper
|
|
* @param {Object} mapper - VTK mapper
|
|
* @param {string} colormapName - Colormap preset name
|
|
* @param {number} minVal - Range minimum
|
|
* @param {number} maxVal - Range maximum
|
|
*/
|
|
applyColormap(mapper, colormapName, minVal, maxVal) {
|
|
try {
|
|
const lookupTable = this.vtkColorTransferFunction.newInstance();
|
|
const preset = this.vtkColorMaps.getPresetByName(colormapName);
|
|
|
|
if (preset) {
|
|
lookupTable.applyColorMap(preset);
|
|
lookupTable.setMappingRange(minVal, maxVal);
|
|
lookupTable.updateRange();
|
|
mapper.setLookupTable(lookupTable);
|
|
} else {
|
|
console.warn(`[FieldVisualization] Colormap "${colormapName}" not found`);
|
|
}
|
|
} catch (error) {
|
|
console.error('[FieldVisualization] Error applying colormap:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable scalar visualization on a mapper
|
|
* @param {Object} mapper - VTK mapper
|
|
* @param {boolean} render - Whether to trigger render
|
|
*/
|
|
disableScalarVisualization(mapper, render = true) {
|
|
if (mapper) {
|
|
mapper.setScalarVisibility(false);
|
|
}
|
|
this.currentField = null;
|
|
if (render) {
|
|
this.renderWindow.render();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available colormap presets
|
|
* @param {boolean} popularFirst - Put popular colormaps at top (default: true)
|
|
* @returns {Array} Array of {value, label, isPopular} objects
|
|
*/
|
|
getColormapOptions(popularFirst = true) {
|
|
const allPresets = this.vtkColorMaps.rgbPresetNames || [];
|
|
const options = [];
|
|
|
|
if (popularFirst) {
|
|
// Add popular colormaps first
|
|
POPULAR_COLORMAPS.forEach(name => {
|
|
if (allPresets.includes(name)) {
|
|
options.push({ value: name, label: name, isPopular: true });
|
|
}
|
|
});
|
|
|
|
// Add separator
|
|
options.push({ value: '', label: '----------', disabled: true });
|
|
|
|
// Add remaining colormaps
|
|
allPresets
|
|
.filter(name => !POPULAR_COLORMAPS.includes(name))
|
|
.sort()
|
|
.forEach(name => {
|
|
options.push({ value: name, label: name, isPopular: false });
|
|
});
|
|
} else {
|
|
allPresets.sort().forEach(name => {
|
|
options.push({ value: name, label: name, isPopular: POPULAR_COLORMAPS.includes(name) });
|
|
});
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Set auto range mode
|
|
* @param {boolean} auto - Whether to use auto range
|
|
*/
|
|
setAutoRange(auto) {
|
|
this.autoRange = auto;
|
|
}
|
|
|
|
/**
|
|
* Set manual range
|
|
* @param {number} min - Range minimum
|
|
* @param {number} max - Range maximum
|
|
*/
|
|
setManualRange(min, max) {
|
|
this.manualRange = [min, max];
|
|
}
|
|
|
|
/**
|
|
* Calculate synchronized range for multiple polydatas
|
|
* @param {Object[]} polydatas - Array of VTK polydata objects
|
|
* @param {string} fieldName - Field name
|
|
* @returns {number[]} Combined range [min, max]
|
|
*/
|
|
calculateSynchronizedRange(polydatas, fieldName) {
|
|
let globalMin = Infinity;
|
|
let globalMax = -Infinity;
|
|
|
|
const isCellData = fieldName.startsWith('cell:');
|
|
const actualFieldName = isCellData
|
|
? fieldName.substring(5)
|
|
: fieldName.startsWith('point:')
|
|
? fieldName.substring(6)
|
|
: fieldName;
|
|
|
|
polydatas.forEach(polyData => {
|
|
if (!polyData) return;
|
|
|
|
const data = isCellData ? polyData.getCellData() : polyData.getPointData();
|
|
const scalarArray = data?.getArrayByName(actualFieldName);
|
|
|
|
if (scalarArray) {
|
|
const range = scalarArray.getRange();
|
|
globalMin = Math.min(globalMin, range[0]);
|
|
globalMax = Math.max(globalMax, range[1]);
|
|
}
|
|
});
|
|
|
|
if (globalMin === Infinity || globalMax === -Infinity) {
|
|
return [0, 1]; // Default range
|
|
}
|
|
|
|
return [globalMin, globalMax];
|
|
}
|
|
|
|
/**
|
|
* Find common fields between multiple polydatas
|
|
* @param {Object[]} polydatas - Array of VTK polydata objects
|
|
* @returns {Object} Object with commonPointFields, commonCellFields, commonAllFields
|
|
*/
|
|
findCommonFields(polydatas) {
|
|
if (!polydatas || polydatas.length === 0) {
|
|
return { commonPointFields: [], commonCellFields: [], commonAllFields: [] };
|
|
}
|
|
|
|
// Get fields from first polydata
|
|
const firstFields = this.extractFields(polydatas[0]);
|
|
let commonPointFields = firstFields.pointFields.map(f => f.name);
|
|
let commonCellFields = firstFields.cellFields.map(f => f.name);
|
|
|
|
// Intersect with remaining polydatas
|
|
for (let i = 1; i < polydatas.length; i++) {
|
|
const fields = this.extractFields(polydatas[i]);
|
|
const pointNames = fields.pointFields.map(f => f.name);
|
|
const cellNames = fields.cellFields.map(f => f.name);
|
|
|
|
commonPointFields = commonPointFields.filter(n => pointNames.includes(n));
|
|
commonCellFields = commonCellFields.filter(n => cellNames.includes(n));
|
|
}
|
|
|
|
return {
|
|
commonPointFields,
|
|
commonCellFields,
|
|
commonAllFields: [...commonPointFields, ...commonCellFields]
|
|
};
|
|
}
|
|
}
|
|
|
|
export default FieldVisualization;
|