diff --git a/inst/htmlwidgets/lib/neurosurface/neurosurface.js b/inst/htmlwidgets/lib/neurosurface/neurosurface.js index a4bb71c..1fd4aef 100644 --- a/inst/htmlwidgets/lib/neurosurface/neurosurface.js +++ b/inst/htmlwidgets/lib/neurosurface/neurosurface.js @@ -1,3 +1,40 @@ + + setData(newData) { + const isArray = Array.isArray(newData) || ArrayBuffer.isView(newData); + if (!isArray) { + console.error('setData expects an array or typed array of numbers'); + return; + } + if (newData.length !== this.data.length) { + console.error(`New data length (${newData.length}) does not match the current data length (${this.data.length})`); + return; + } + this.data = ArrayBuffer.isView(newData) ? new Float32Array(newData) : Float32Array.from(newData); + this.updateColors(); + } + setColors(newColors) { + if (!Array.isArray(newColors)) { + console.error('setColors expects an array of color strings'); + return; + } + if (newColors.length !== this.indices.length) { + console.error(`Colors array length (${newColors.length}) does not match the number of indices (${this.indices.length})`); + return; + } + + this.colors = new Float32Array(newColors.length * 3); + for (let i = 0; i < newColors.length; i++) { + if (typeof newColors[i] !== 'string') { + console.error(`Color at index ${i} is not a valid string`); + return; + } + const color = new Color(newColors[i]); + this.colors[i * 3] = color.r; + this.colors[i * 3 + 1] = color.g; + this.colors[i * 3 + 2] = color.b; + } + this.updateColors(); + this.mouse = new Vector2(); this.intersectionPoint = new Vector3(); @@ -39,4 +76,5 @@ this.paneContainer = null; } } + } diff --git a/inst/htmlwidgets/neurosurface/src/ColorMap.js b/inst/htmlwidgets/neurosurface/src/ColorMap.js index 7472323..c3d37de 100644 --- a/inst/htmlwidgets/neurosurface/src/ColorMap.js +++ b/inst/htmlwidgets/neurosurface/src/ColorMap.js @@ -1,5 +1,6 @@ import colormap from 'colormap'; import { EventEmitter } from './EventEmitter.js'; +import { debugLog } from './debug.js'; class ColorMap extends EventEmitter { constructor(colors, options = {}) { @@ -18,7 +19,7 @@ class ColorMap extends EventEmitter { setRange(range) { if (Array.isArray(range) && range.length === 2 && range.every(v => typeof v === 'number')) { this.range = range; - console.log('ColorMap: Emitting rangeChanged event', this.range); + debugLog('ColorMap: Emitting rangeChanged event', this.range); this.emit('rangeChanged', this.range); } else { this.range = [0, 1]; @@ -28,7 +29,7 @@ class ColorMap extends EventEmitter { setThreshold(threshold) { if (Array.isArray(threshold) && threshold.length === 2 && threshold.every(v => typeof v === 'number')) { this.threshold = threshold; - console.log('ColorMap: Emitting thresholdChanged event', this.threshold); + debugLog('ColorMap: Emitting thresholdChanged event', this.threshold); this.emit('thresholdChanged', this.threshold); } else { this.threshold = [0, 0]; diff --git a/inst/htmlwidgets/neurosurface/src/EventEmitter.js b/inst/htmlwidgets/neurosurface/src/EventEmitter.js index 61d1c05..5f67439 100644 --- a/inst/htmlwidgets/neurosurface/src/EventEmitter.js +++ b/inst/htmlwidgets/neurosurface/src/EventEmitter.js @@ -1,9 +1,13 @@ export class EventEmitter { constructor() { - this._events = {}; + // Use an object without a prototype to avoid prototype pollution + this._events = Object.create(null); } on(event, listener) { + if (typeof listener !== 'function') { + throw new TypeError('listener must be a function'); + } if (!this._events[event]) { this._events[event] = []; } @@ -11,9 +15,21 @@ export class EventEmitter { return () => this.removeListener(event, listener); } + once(event, listener) { + if (typeof listener !== 'function') { + throw new TypeError('listener must be a function'); + } + const wrapped = (...args) => { + this.removeListener(event, wrapped); + listener(...args); + }; + return this.on(event, wrapped); + } + emit(event, ...args) { if (this._events[event]) { - this._events[event].forEach((listener) => listener(...args)); + // Copy listeners to avoid issues if the array is modified during emit + [...this._events[event]].forEach((listener) => listener(...args)); } } @@ -22,6 +38,17 @@ export class EventEmitter { this._events[event] = this._events[event].filter( (listener) => listener !== listenerToRemove ); + if (this._events[event].length === 0) { + delete this._events[event]; + } + } + } + + removeAllListeners(event) { + if (event) { + delete this._events[event]; + } else { + this._events = Object.create(null); } } -} \ No newline at end of file +} diff --git a/inst/htmlwidgets/neurosurface/src/NeuroSurfaceViewer.js b/inst/htmlwidgets/neurosurface/src/NeuroSurfaceViewer.js index 8215e61..54fcbc5 100644 --- a/inst/htmlwidgets/neurosurface/src/NeuroSurfaceViewer.js +++ b/inst/htmlwidgets/neurosurface/src/NeuroSurfaceViewer.js @@ -3,6 +3,7 @@ import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls import { ColorMappedNeuroSurface, VertexColoredNeuroSurface } from './classes.js'; import { Pane } from 'tweakpane'; import * as EssentialsPlugin from '@tweakpane/plugin-essentials'; +import { debugLog } from './debug.js'; export class NeuroSurfaceViewer { constructor(container, width, height, config = {}, viewpoint = 'lateral') { @@ -265,9 +266,9 @@ export class NeuroSurfaceViewer { umat = new THREE.Matrix4().identity(); } - console.log('Setting viewpoint to:', viewpoint); - console.log('Current viewpoint state:', this.viewpointState); - console.log("umat", umat); + debugLog('Setting viewpoint to:', viewpoint); + debugLog('Current viewpoint state:', this.viewpointState); + debugLog('umat', umat); // Calculate the bounding box of all surfaces const box = new THREE.Box3(); @@ -334,7 +335,7 @@ export class NeuroSurfaceViewer { } updateIntensityRange() { - console.log('NeuroSurfaceViewer: Updating intensity range', [this.intensityRange.range.min, this.intensityRange.range.max]); + debugLog('NeuroSurfaceViewer: Updating intensity range', [this.intensityRange.range.min, this.intensityRange.range.max]); this.surfaces.forEach(surface => { if (surface instanceof ColorMappedNeuroSurface) { surface.colorMap.setRange([this.intensityRange.range.min, this.intensityRange.range.max]); @@ -344,7 +345,7 @@ export class NeuroSurfaceViewer { } updateThresholdRange() { - console.log('NeuroSurfaceViewer: Updating threshold range', [this.thresholdRange.range.min, this.thresholdRange.range.max]); + debugLog('NeuroSurfaceViewer: Updating threshold range', [this.thresholdRange.range.min, this.thresholdRange.range.max]); this.surfaces.forEach(surface => { if (surface instanceof ColorMappedNeuroSurface) { surface.colorMap.setThreshold([this.thresholdRange.range.min, this.thresholdRange.range.max]); @@ -361,7 +362,7 @@ export class NeuroSurfaceViewer { } addSurface(surface, id) { - console.log('Adding surface:', surface, 'with id:', id); + debugLog('Adding surface:', surface, 'with id:', id); this.surfaces.set(id, surface); if (!surface.mesh) { console.warn('Surface mesh not created. Creating now.'); @@ -370,7 +371,7 @@ export class NeuroSurfaceViewer { this.scene.add(surface.mesh); if (surface instanceof ColorMappedNeuroSurface) { - console.log('Updating data range for ColorMappedNeuroSurface'); + debugLog('Updating data range for ColorMappedNeuroSurface'); this.updateDataRange(surface.data); this.updateRangeControls(); } @@ -481,7 +482,7 @@ export class NeuroSurfaceViewer { this.updateRangeControls(); surface.updateColors(); this.render(); - console.log(`Updated data for surface with id: ${id}`); + debugLog(`Updated data for surface with id: ${id}`); } else { console.warn(`No ColorMappedNeuroSurface found with id: ${id}`); } @@ -492,7 +493,7 @@ export class NeuroSurfaceViewer { if (surface && surface instanceof VertexColoredNeuroSurface) { surface.setColors(colors); this.render(); - console.log(`Updated colors for surface with id: ${id}`); + debugLog(`Updated colors for surface with id: ${id}`); } else { console.warn(`No VertexColoredNeuroSurface found with id: ${id}`); } diff --git a/inst/htmlwidgets/neurosurface/src/classes.js b/inst/htmlwidgets/neurosurface/src/classes.js index b4df5d6..8fa1254 100644 --- a/inst/htmlwidgets/neurosurface/src/classes.js +++ b/inst/htmlwidgets/neurosurface/src/classes.js @@ -1,5 +1,6 @@ import * as THREE from 'three'; import ColorMap from './ColorMap.js'; +import { debugLog } from './debug.js'; export class SurfaceGeometry { constructor(vertices, faces, hemi) { @@ -8,10 +9,10 @@ export class SurfaceGeometry { this.hemi = hemi; this.mesh = null; - console.log("SurfaceGeometry constructor called"); - console.log("Vertices:", this.vertices.length); - console.log("Faces:", this.faces.length); - console.log("Hemi:", this.hemi); + debugLog('SurfaceGeometry constructor called'); + debugLog('Vertices:', this.vertices.length); + debugLog('Faces:', this.faces.length); + debugLog('Hemi:', this.hemi); this.createMesh(); } @@ -28,8 +29,8 @@ export class SurfaceGeometry { }); this.mesh = new THREE.Mesh(geometry, material); - console.log("SurfaceGeometry construction complete"); - console.log("Mesh:", this.mesh); + debugLog('SurfaceGeometry construction complete'); + debugLog('Mesh:', this.mesh); } } @@ -195,17 +196,17 @@ export class ColorMappedNeuroSurface extends NeuroSurface { // Set up new listeners this.rangeListener = this.colorMap.on('rangeChanged', (range) => { - console.log('ColorMappedNeuroSurface: Received rangeChanged event', range); + debugLog('ColorMappedNeuroSurface: Received rangeChanged event', range); this.irange = range; this.updateColors(); }); this.thresholdListener = this.colorMap.on('thresholdChanged', (threshold) => { - console.log('ColorMappedNeuroSurface: Received thresholdChanged event', threshold); + debugLog('ColorMappedNeuroSurface: Received thresholdChanged event', threshold); this.threshold = threshold; this.updateColors(); }); this.alphaListener = this.colorMap.on('alphaChanged', (alpha) => { - console.log('ColorMappedNeuroSurface: Received alphaChanged event', alpha); + debugLog('ColorMappedNeuroSurface: Received alphaChanged event', alpha); this.config.alpha = alpha; this.updateColors(); }); @@ -239,11 +240,11 @@ export class ColorMappedNeuroSurface extends NeuroSurface { } updateColors() { - console.log('Updating colors. Mesh:', !!this.mesh, 'ColorMap:', !!this.colorMap); + debugLog('Updating colors. Mesh:', !!this.mesh, 'ColorMap:', !!this.colorMap); if (!this.mesh || !this.colorMap) { console.warn('Mesh or ColorMap not initialized in updateColors'); - console.log('Mesh:', this.mesh); - console.log('ColorMap:', this.colorMap); + debugLog('Mesh:', this.mesh); + debugLog('ColorMap:', this.colorMap); return; } @@ -251,10 +252,10 @@ export class ColorMappedNeuroSurface extends NeuroSurface { const componentsPerColor = 4; // Always use RGBA const colors = new Float32Array(vertexCount * componentsPerColor); - console.log("threshold", this.threshold); - console.log("irange", this.irange); - console.log("alpha", this.config.alpha); - console.log("data", this.data); + debugLog('threshold', this.threshold); + debugLog('irange', this.irange); + debugLog('alpha', this.config.alpha); + debugLog('data', this.data); const baseSurfaceColor = new THREE.Color(this.config.color); @@ -315,11 +316,16 @@ export class ColorMappedNeuroSurface extends NeuroSurface { } setData(newData) { + const isArray = Array.isArray(newData) || ArrayBuffer.isView(newData); + if (!isArray) { + console.error('setData expects an array or typed array of numbers'); + return; + } if (newData.length !== this.data.length) { - console.error('New data length does not match the current data length'); + console.error(`New data length (${newData.length}) does not match the current data length (${this.data.length})`); return; } - this.data = newData; + this.data = ArrayBuffer.isView(newData) ? new Float32Array(newData) : Float32Array.from(newData); this.updateColors(); } } @@ -331,8 +337,21 @@ export class VertexColoredNeuroSurface extends NeuroSurface { } setColors(newColors) { + if (!Array.isArray(newColors)) { + console.error('setColors expects an array of color strings'); + return; + } + if (newColors.length !== this.indices.length) { + console.error(`Colors array length (${newColors.length}) does not match the number of indices (${this.indices.length})`); + return; + } + this.colors = new Float32Array(newColors.length * 3); for (let i = 0; i < newColors.length; i++) { + if (typeof newColors[i] !== 'string') { + console.error(`Color at index ${i} is not a valid string`); + return; + } const color = new THREE.Color(newColors[i]); this.colors[i * 3] = color.r; this.colors[i * 3 + 1] = color.g; diff --git a/inst/htmlwidgets/neurosurface/src/debug.js b/inst/htmlwidgets/neurosurface/src/debug.js new file mode 100644 index 0000000..a5b1b52 --- /dev/null +++ b/inst/htmlwidgets/neurosurface/src/debug.js @@ -0,0 +1,9 @@ +export let DEBUG = false; +export function setDebug(value) { + DEBUG = value; +} +export function debugLog(...args) { + if (DEBUG) { + console.log(...args); + } +} diff --git a/inst/htmlwidgets/neurosurface/src/index.js b/inst/htmlwidgets/neurosurface/src/index.js index 25d7fda..d504198 100644 --- a/inst/htmlwidgets/neurosurface/src/index.js +++ b/inst/htmlwidgets/neurosurface/src/index.js @@ -1,6 +1,7 @@ import * as THREE from 'three'; import { NeuroSurfaceViewer } from './NeuroSurfaceViewer'; import { SurfaceGeometry, NeuroSurface, ColorMappedNeuroSurface, VertexColoredNeuroSurface } from './classes'; +import { debugLog, setDebug } from './debug.js'; // Export the classes so they're available to the widget export { @@ -9,7 +10,9 @@ export { NeuroSurface, ColorMappedNeuroSurface, VertexColoredNeuroSurface, - THREE + THREE, + debugLog, + setDebug }; // Optionally, you can also attach these to the global window object @@ -21,8 +24,9 @@ if (typeof window !== 'undefined') { NeuroSurface, ColorMappedNeuroSurface, VertexColoredNeuroSurface, - THREE + THREE, + debugLog, + setDebug }; } - -console.log('Neurosurface module initialized'); +debugLog('Neurosurface module initialized'); diff --git a/inst/htmlwidgets/surfwidget.js b/inst/htmlwidgets/surfwidget.js index 7c52539..355b6e8 100644 --- a/inst/htmlwidgets/surfwidget.js +++ b/inst/htmlwidgets/surfwidget.js @@ -12,7 +12,7 @@ HTMLWidgets.widget({ renderValue: function(x) { if (!viewer) { - console.log("Creating NeuroSurfaceViewer"); + neurosurface.debugLog('Creating NeuroSurfaceViewer'); viewer = new neurosurface.NeuroSurfaceViewer(el, width, height, { ...x.config, cmap: x.cmap, @@ -26,13 +26,13 @@ HTMLWidgets.widget({ } try { - console.log("Creating SurfaceGeometry"); + neurosurface.debugLog('Creating SurfaceGeometry'); var geometry = new neurosurface.SurfaceGeometry(x.vertices, x.faces, x.hemi); - console.log("SurfaceGeometry created:", geometry); + neurosurface.debugLog('SurfaceGeometry created:', geometry); var surface; if (x.cmap) { - console.log("Creating ColorMappedNeuroSurface"); + neurosurface.debugLog('Creating ColorMappedNeuroSurface'); surface = new neurosurface.ColorMappedNeuroSurface( geometry, x.indices, @@ -41,7 +41,7 @@ HTMLWidgets.widget({ { irange: x.irange, thresh: x.thresh, alpha: x.alpha, ...x.config } ); } else if (x.vertexColors) { - console.log("Creating VertexColoredNeuroSurface"); + neurosurface.debugLog('Creating VertexColoredNeuroSurface'); surface = new neurosurface.VertexColoredNeuroSurface( geometry, x.indices, @@ -52,8 +52,8 @@ HTMLWidgets.widget({ throw new Error("Neither color map nor vertex colors provided"); } - console.log("Surface created:", surface); - console.log("Adding surface to viewer"); + neurosurface.debugLog('Surface created:', surface); + neurosurface.debugLog('Adding surface to viewer'); viewer.addSurface(surface, surfaceId); viewer.animate();