Files
siraxe-ComfyUI-WanVideoWrap…/web/power_spline_editor/layer_renderer.js
T
2025-10-09 19:27:37 +01:00

325 lines
14 KiB
JavaScript

/**
* @class LayerRenderer
* A dedicated class to handle the rendering of active and inactive spline layers
* on the Protovis canvas. This centralizes drawing logic and state management
* for rendering, making it more robust and easier to maintain.
*/
export class LayerRenderer {
constructor(splineEditor, vis) {
this.splineEditor = splineEditor;
this.vis = vis;
this.node = splineEditor.node;
// Track individual panels for each inactive layer to ensure complete isolation
// Each layer gets its own panel to prevent state pollution and rendering artifacts
this.inactiveLayerPanels = [];
this.activeLayerPanel = this.vis.add(pv.Panel);
}
/**
* Main render method. Clears previous drawings and redraws all layers.
*/
render() {
this.clearLayers();
const activeWidget = this.node.layerManager.getActiveWidget();
const allWidgets = this.node.layerManager.getSplineWidgets();
// Render inactive and off layers - each in its own isolated panel
allWidgets.forEach(widget => {
if (widget !== activeWidget) {
this.drawInactiveLayer(widget);
}
});
// Draw the active layer on top in its dedicated panel
if (activeWidget) {
this.drawActiveLayer(activeWidget);
}
// Ensure active panel is always the last child so it renders on top
// This prevents inactive layers from blocking mouse events when overlapping
const activePanelIndex = this.vis.children.indexOf(this.activeLayerPanel);
if (activePanelIndex > -1) {
this.vis.children.splice(activePanelIndex, 1);
this.vis.children.push(this.activeLayerPanel);
}
// Apply the changes to the SVG
this.vis.render();
}
/**
* Clears all drawings from the layer panels.
* Properly destroys individual inactive layer panels to prevent memory leaks.
*/
clearLayers() {
// Remove all inactive layer panels from the vis
this.inactiveLayerPanels.forEach(panel => {
const index = this.vis.children.indexOf(panel);
if (index > -1) {
this.vis.children.splice(index, 1);
}
});
// Clear the array
this.inactiveLayerPanels = [];
// Clear active layer panel
this.activeLayerPanel.children = [];
}
/**
* Draws the currently active spline layer.
* This includes the main line, control points with drag behaviors, and highlights.
* Uses closures to ensure drag events work with actual point objects.
* @param {object} widget - The active spline widget.
*/
drawActiveLayer(widget) {
const points = this.splineEditor.points;
if (!points || points.length === 0) return;
const interpolation = widget.value.interpolation || 'linear';
// ALWAYS draw line - thin for points mode, normal otherwise
// Points mode uses linear interpolation with a thin line
// For non-points interpolation, we need to handle highlighted points specially
this.activeLayerPanel.add(pv.Line)
.data(() => {
// Special handling for curve rendering when points are highlighted
if (interpolation !== 'points' && interpolation !== 'linear') {
const hasAnyHighlighted = points.some(p => p.highlighted);
if (hasAnyHighlighted) {
// Insert duplicate points at highlighted positions to force linear segments
const renderPoints = [];
for (let i = 0; i < points.length; i++) {
const point = points[i];
if (point.highlighted) {
// Add a point slightly before (for incoming linear segment)
if (i > 0) {
const prevPoint = points[i - 1];
const dx = point.x - prevPoint.x;
const dy = point.y - prevPoint.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offset = Math.min(0.01, 1 / dist); // Very small offset
renderPoints.push({
x: prevPoint.x + dx * (1 - offset),
y: prevPoint.y + dy * (1 - offset)
});
}
// Add the highlighted point itself
renderPoints.push(point);
// Add a point slightly after (for outgoing linear segment)
if (i < points.length - 1) {
const nextPoint = points[i + 1];
const dx = nextPoint.x - point.x;
const dy = nextPoint.y - point.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offset = Math.min(0.01, 1 / dist); // Very small offset
renderPoints.push({
x: point.x + dx * offset,
y: point.y + dy * offset
});
}
} else {
renderPoints.push(point);
}
}
return renderPoints;
}
}
return points;
})
.left(d => d.x)
.top(d => d.y)
.interpolate(() => interpolation === 'points' ? 'linear' : interpolation)
.strokeStyle(() => interpolation === 'points' ? "#1f77b4" : "#1f77b4")
.lineWidth(() => interpolation === 'points' ? 1 : 3);
// ALWAYS draw dots - bigger for points mode, all circles
// Use closures to ensure drag handlers can find points via indexOf()
this.activeLayerPanel.add(pv.Dot)
.data(() => points)
.left(d => d.x)
.top(d => d.y)
.radius(() => interpolation === 'points' ? 9 : 6)
.shape((dot) => {
if (interpolation === 'points') return "circle";
// If interpolation is 'basis' AND the point is highlighted, draw a square
if (interpolation === 'basis' && dot.highlighted) return "square";
const index = this.splineEditor.points.indexOf(dot);
return index === 0 ? "triangle" : "circle";
}) .angle((dot) => {
if (interpolation === 'points') return 0;
const index = this.splineEditor.points.indexOf(dot);
if (index !== 0 || points.length <= 1) return 0;
const dx = points[1].x - points[0].x;
const dy = points[1].y - points[0].y;
if (dx !== 0 || dy !== 0) {
const angle = Math.atan2(dy, dx) - Math.PI / 2;
return (angle + 2 * Math.PI) % (2 * Math.PI);
}
return 0;
})
.cursor("move")
.strokeStyle((dot) => {
const index = this.splineEditor.points.indexOf(dot);
if (index === 0 && interpolation !== 'points') {
return "green";
}
return interpolation === 'points' ? "#139613" : "#1f77b4";
})
.fillStyle((dot) => {
const index = this.splineEditor.points.indexOf(dot);
if (index === 0 && interpolation !== 'points') {
return "rgba(0, 255, 0, 0.5)";
}
return interpolation === 'points' ? "rgba(19, 150, 19, 0.5)" : "rgba(100, 100, 100, 0.5)";
})
.event("mousedown", pv.Behavior.drag())
.event("dragstart", this.splineEditor.dragStartHandler)
.event("dragend", this.splineEditor.dragEndHandler)
.event("drag", this.splineEditor.dragHandler)
.event("mouseover", this.splineEditor.mouseOverHandler)
.event("mouseout", this.splineEditor.mouseOutHandler)
.event("mouseup", function() {
// Handle right-click (button 2) to toggle highlight on points (except first point)
if (pv.event.button === 2 && !pv.event.altKey) {
const dot = this;
const index = this.splineEditor.points.indexOf(dot);
if (index !== 0 && index !== -1) {
// Toggle the highlighted state for the clicked point
this.splineEditor.points[index].highlighted = !this.splineEditor.points[index].highlighted;
// Trigger re-render to update the visual representation
this.splineEditor.layerRenderer.render();
}
}
});
// No labels for any mode - removed the label anchor
}
/**
* Draws an inactive or off spline layer.
* Each layer gets its own isolated panel to prevent state pollution.
* @param {object} widget - The inactive or off spline widget.
*/
drawInactiveLayer(widget) {
const isOff = !widget.value.on;
const strokeColor = isOff ? "rgba(255, 255, 255, 0.1)" : "rgba(255, 127, 14, 0.5)";
const fillColor = isOff ? "rgba(255, 255, 255, 0.1)" : "rgba(255, 127, 14, 0.4)";
let points;
try {
const storedPoints = JSON.parse(widget.value.points_store || '[]');
points = this.splineEditor.denormalizePoints(storedPoints);
} catch (e) {
console.error("Error parsing points for inactive layer:", e);
return;
}
if (!points || points.length === 0) return;
// Create a new isolated panel for this layer
const layerPanel = this.vis.add(pv.Panel);
this.inactiveLayerPanels.push(layerPanel);
const interpolation = widget.value.interpolation || 'linear';
// Snapshot points to avoid closure issues
const pointsSnapshot = [...points];
// For inactive layers, we also need to handle highlighted points for consistency
let renderPoints = pointsSnapshot;
if (interpolation !== 'points' && interpolation !== 'linear') {
const hasAnyHighlighted = points.some(p => p.highlighted);
if (hasAnyHighlighted) {
// Insert duplicate points at highlighted positions to force linear segments
const tempRenderPoints = [];
for (let i = 0; i < points.length; i++) {
const point = points[i];
if (point.highlighted) {
if (i > 0) {
const prevPoint = points[i - 1];
const dx = point.x - prevPoint.x;
const dy = point.y - prevPoint.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offset = Math.min(0.01, 1 / dist);
tempRenderPoints.push({ x: prevPoint.x + dx * (1 - offset), y: prevPoint.y + dy * (1 - offset), _helper: true });
}
tempRenderPoints.push(point);
if (i < points.length - 1) {
const nextPoint = points[i + 1];
const dx = nextPoint.x - point.x;
const dy = nextPoint.y - point.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const offset = Math.min(0.01, 1 / dist);
tempRenderPoints.push({ x: point.x + dx * offset, y: point.y + dy * offset, _helper: true });
}
} else {
tempRenderPoints.push(point);
}
}
renderPoints = tempRenderPoints;
}
}
// ALWAYS draw line
layerPanel.add(pv.Line)
.data(renderPoints)
.left(d => d.x)
.top(d => d.y)
.interpolate(interpolation === 'points' ? 'linear' : interpolation)
.strokeStyle(strokeColor)
.lineWidth(interpolation === 'points' ? 1 : 3);
// Handle different interpolation modes for inactive layers
if (interpolation === 'points') {
layerPanel.add(pv.Line)
.data(pointsSnapshot)
.left(d => d.x)
.top(d => d.y)
.interpolate('linear')
.strokeStyle(strokeColor)
.lineWidth(1);
layerPanel.add(pv.Dot)
.data(pointsSnapshot)
.left(d => d.x)
.top(d => d.y)
.radius(6)
.shape("circle")
.strokeStyle(strokeColor)
.fillStyle(fillColor);
} else {
// ONLY draw triangle at first point for all other modes
if (pointsSnapshot.length > 0) {
layerPanel.add(pv.Dot)
.data([pointsSnapshot[0]])
.left(d => d.x)
.top(d => d.y)
.radius(6)
.shape("triangle")
.strokeStyle(strokeColor)
.fillStyle(fillColor)
.angle((dot) => {
const points = pointsSnapshot;
if (points.length <= 1) return 0;
const firstPoint = points[0];
const secondPoint = points[1];
const dx = secondPoint.x - firstPoint.x;
const dy = secondPoint.y - firstPoint.y;
if (dx !== 0 || dy !== 0) {
const angle = Math.atan2(dy, dx) - Math.PI / 2;
return (angle + 2 * Math.PI) % (2 * Math.PI);
}
return 0;
});
}
}
}
}