Files
PozzettiAndrea-ComfyUI-Geom…/javascript/js/viewer/features/FieldVisualization.js
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

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;