layer protoviz bugfix

This commit is contained in:
JJ
2025-11-08 16:37:06 +00:00
parent 44e89c444a
commit 967feb6673
4 changed files with 159 additions and 44 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ nodes/__pycache__/
.claude-code-router
.env
.env2
.env 22
opencode.json
wanvideo_cached_samples.pt
wanvideo_cached_videos.pt
ref_image.jpg
+17 -8
View File
@@ -377,11 +377,15 @@ export class LayerRenderer {
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);
const index = this.splineEditor.resolvePointIndex
? this.splineEditor.resolvePointIndex(dot)
: this.splineEditor.points.indexOf(dot);
return index === 0 ? "triangle" : "circle";
}) .angle((dot) => {
if (interpolation === 'points') return 0;
const index = this.splineEditor.points.indexOf(dot);
const index = this.splineEditor.resolvePointIndex
? this.splineEditor.resolvePointIndex(dot)
: this.splineEditor.points.indexOf(dot);
if (index !== 0 || points.length <= 1) return 0;
const dx = points[1].x - points[0].x;
@@ -394,7 +398,9 @@ export class LayerRenderer {
})
.cursor("move")
.strokeStyle((dot) => {
const index = this.splineEditor.points.indexOf(dot);
const index = this.splineEditor.resolvePointIndex
? this.splineEditor.resolvePointIndex(dot)
: this.splineEditor.points.indexOf(dot);
if (!isHanddraw && index === 0 && interpolation !== 'points') {
return "green";
}
@@ -402,17 +408,20 @@ export class LayerRenderer {
return interpolation === 'points' ? "#139613" : "#1f77b4";
})
.fillStyle((dot) => {
const index = this.splineEditor.points.indexOf(dot);
const index = this.splineEditor.resolvePointIndex
? this.splineEditor.resolvePointIndex(dot)
: this.splineEditor.points.indexOf(dot);
if (!isHanddraw && index === 0 && interpolation !== 'points') {
return "rgba(0, 255, 0, 0.5)";
}
if (isHanddraw) return "rgba(215, 196, 0, 0.45)";
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("mousedown", (dot) => {
if (this.splineEditor.handlePointPointerDown) {
this.splineEditor.handlePointPointerDown(dot, pv.event);
}
})
.event("mouseover", this.splineEditor.mouseOverHandler)
.event("mouseout", this.splineEditor.mouseOutHandler);
// No labels for any mode - removed the label anchor
+30 -15
View File
@@ -286,6 +286,9 @@ class SplineLayerManager {
}
addNewSpline(name) {
try {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const widget = new PowerSplineWidget("spline_" + this.node.splineWidgetsCounter);
@@ -317,10 +320,11 @@ class SplineLayerManager {
// This ensures order: canvas → button_bar → header → splines
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
// Find the last spline widget after the header
// Find the last layer widget (spline or handdraw) after the header
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
if (this.node.widgets[i] instanceof PowerSplineWidget) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget) {
insertIndex = i + 1;
} else {
break;
@@ -481,6 +485,9 @@ class SplineLayerManager {
duplicateSpline(sourceWidget) {
if (!sourceWidget) return;
try {
this.node?.editor?.exitHanddrawMode?.(false);
} catch {}
this.node.splineWidgetsCounter++;
const newWidget = new PowerSplineWidget("spline_" + this.node.splineWidgetsCounter);
@@ -506,7 +513,8 @@ class SplineLayerManager {
if (headerIndex !== -1) {
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
if (this.node.widgets[i] instanceof PowerSplineWidget) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget) {
insertIndex = i + 1;
} else {
break;
@@ -598,10 +606,11 @@ class SplineLayerManager {
// Insert spline widgets AFTER the header (same as addNewSpline logic)
const headerIndex = this.node.widgets.findIndex(w => w.name === "spline_header");
if (headerIndex !== -1) {
// Find the last spline widget after the header
// Find the last layer widget after the header
let insertIndex = headerIndex + 1;
for (let i = headerIndex + 1; i < this.node.widgets.length; i++) {
if (this.node.widgets[i] instanceof PowerSplineWidget) {
const wi = this.node.widgets[i];
if (wi instanceof PowerSplineWidget || wi instanceof HandDrawLayerWidget) {
insertIndex = i + 1;
} else {
break;
@@ -1533,17 +1542,23 @@ app.registerExtension({
const addSplineWidth = (width - margin * 2 - gap) * 0.70;
const duplicateWidth = (width - margin * 2 - gap) * 0.30;
// Handle Add Spline button click
if (this.addSplineMouseDown && pos[0] >= margin && pos[0] <= margin + addSplineWidth) {
node.layerManager.addNewSpline();
// Handle Add Spline button click
if (this.addSplineMouseDown && pos[0] >= margin && pos[0] <= margin + addSplineWidth) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
// Handle Duplicate button click
else if (this.duplicateMouseDown &&
pos[0] >= margin + addSplineWidth + gap &&
pos[0] <= margin + addSplineWidth + gap + duplicateWidth) {
const activeWidget = node.layerManager.getActiveWidget();
if (activeWidget) {
node.layerManager.duplicateSpline(activeWidget);
node.layerManager.addNewSpline();
}
// Handle Duplicate button click
else if (this.duplicateMouseDown &&
pos[0] >= margin + addSplineWidth + gap &&
pos[0] <= margin + addSplineWidth + gap + duplicateWidth) {
if (node?.editor && node.editor._handdrawMode === 'create') {
node.editor.exitHanddrawMode?.(false);
}
const activeWidget = node.layerManager.getActiveWidget();
if (activeWidget) {
node.layerManager.duplicateSpline(activeWidget);
}
}
+111 -20
View File
@@ -499,19 +499,110 @@ export default class SplineEditor2 {
}
})
this.dragStartHandler = (d) => {
const dot = d;
this.i = this.points.indexOf(dot);
this.hoverIndex = this.i;
this.resolvePointIndex = (dot) => {
if (!Array.isArray(this.points) || !dot) {
return -1;
}
const directIdx = this.points.indexOf(dot);
if (directIdx !== -1) {
return directIdx;
}
let fallbackIdx = -1;
let bestDist = Infinity;
const dxDot = dot.x ?? 0;
const dyDot = dot.y ?? 0;
for (let idx = 0; idx < this.points.length; idx++) {
const p = this.points[idx];
if (!p) continue;
const dx = (p.x ?? 0) - dxDot;
const dy = (p.y ?? 0) - dyDot;
const dist = dx * dx + dy * dy;
if (dist <= 1e-4) {
return idx;
}
if (dist < bestDist) {
bestDist = dist;
fallbackIdx = idx;
}
}
return fallbackIdx;
};
const mouseX = this.vis.mouse().x / app.canvas.ds.scale;
const mouseY = this.vis.mouse().y / app.canvas.ds.scale;
this._getPointerCoords = (event) => {
const canvasEl = this.vis?.canvas?.();
if (!canvasEl || !event) {
return { x: 0, y: 0 };
}
const e = (event.touches && event.touches.length > 0) ? event.touches[0] : event;
const rect = canvasEl.getBoundingClientRect();
const scale = app.canvas.ds.scale || 1;
return {
x: (e.clientX - rect.left) / scale,
y: (e.clientY - rect.top) / scale
};
};
this.handlePointPointerDown = (dot, pvEvent) => {
const event = pvEvent || window.event;
if (!event) return;
event.preventDefault?.();
event.stopPropagation?.();
const { x, y } = this._getPointerCoords(event);
this.dragStartHandler(dot, x, y, event);
const needsTracking = this.isDragging || this.isDraggingAll || this.isScalingAll || this.isRotatingAll;
if (!needsTracking) return;
const usePointer = typeof PointerEvent !== 'undefined';
const move = (evt) => {
evt.preventDefault?.();
const coords = this._getPointerCoords(evt);
this.dragHandler(dot, coords.x, coords.y);
};
const end = (evt) => {
cleanup();
this.dragEndHandler(evt);
};
const cleanup = () => {
if (usePointer) {
document.removeEventListener('pointermove', move, true);
document.removeEventListener('pointerup', end, true);
document.removeEventListener('pointercancel', end, true);
} else {
document.removeEventListener('mousemove', move, true);
document.removeEventListener('mouseup', end, true);
}
};
if (usePointer) {
document.addEventListener('pointermove', move, true);
document.addEventListener('pointerup', end, true);
document.addEventListener('pointercancel', end, true);
} else {
document.addEventListener('mousemove', move, true);
document.addEventListener('mouseup', end, true);
}
};
this.dragStartHandler = (d, mouseX, mouseY, rawEvent) => {
const dot = d;
this.i = this.resolvePointIndex(dot);
this.hoverIndex = this.i;
if (this.i === -1 || !this.points[this.i]) {
console.warn("[SplineEditor] dragStartHandler: unable to resolve point for drag", dot);
return;
}
let localX = mouseX;
let localY = mouseY;
if (typeof localX !== 'number' || typeof localY !== 'number') {
const coords = this._getPointerCoords(rawEvent || pv.event);
localX = coords.x;
localY = coords.y;
}
const pointX = this.points[this.i].x;
const pointY = this.points[this.i].y;
this.dragOffset = { x: pointX - mouseX, y: pointY - mouseY };
this.dragOffset = { x: pointX - localX, y: pointY - localY };
// Shift+Middle-Click: Toggle highlighted state on points (except first point)
if (pv.event.shiftKey && pv.event.button === 1) {
if ((rawEvent?.shiftKey) && (rawEvent?.button === 1)) {
if (this.i !== 0 && this.i !== -1) {
this.points[this.i].highlighted = !this.points[this.i].highlighted;
this.layerRenderer.render();
@@ -521,20 +612,20 @@ export default class SplineEditor2 {
}
// Alt+Left-Click: Rotation around clicked point (any point)
if (pv.event.altKey && pv.event.button === 0) {
if ((rawEvent?.altKey) && (rawEvent?.button === 0)) {
this.isRotatingAll = true;
this.originalPoints = this.points.map(p => ({ ...p }));
this.anchorPoint = { ...this.points[this.i] }; // Use clicked point as anchor
this.anchorIndex = this.i; // Store anchor index
this.initialRotationAngle = Math.atan2(mouseY - this.anchorPoint.y, mouseX - this.anchorPoint.x);
this.initialRotationAngle = Math.atan2(localY - this.anchorPoint.y, localX - this.anchorPoint.x);
this.isDragging = true;
return;
}
// Alt + Middle-Click: Translation (any point)
if (pv.event.altKey && pv.event.button === 1) {
if ((rawEvent?.altKey) && (rawEvent?.button === 1)) {
this.isDraggingAll = true;
this.dragStartPos = { x: mouseX, y: mouseY };
this.dragStartPos = { x: localX, y: localY };
// Snapshot original positions to keep a rigid translation anchored at clicked point
this.translateAllSnapshot = {
points: this.points.map(p => ({ ...p })),
@@ -551,12 +642,12 @@ export default class SplineEditor2 {
}
// Alt+Right-Click: Scaling relative to clicked point (any point)
if (pv.event.altKey && pv.event.button === 2) {
if ((rawEvent?.altKey) && (rawEvent?.button === 2)) {
this.isScalingAll = true;
this.originalPoints = this.points.map(p => ({ ...p }));
this.anchorPoint = { ...this.points[this.i] }; // Use clicked point as anchor
this.anchorIndex = this.i; // Store anchor index
this.initialXDistance = mouseX - this.anchorPoint.x;
this.initialXDistance = localX - this.anchorPoint.x;
if (Math.abs(this.initialXDistance) < 10) {
this.initialXDistance = this.initialXDistance >= 0 ? 10 : -10;
}
@@ -566,7 +657,7 @@ export default class SplineEditor2 {
// Regular drag or delete
this.isDragging = true;
if (pv.event.button === 2 && this.i !== 0 && this.i !== this.points.length - 1) {
if ((rawEvent?.button === 2) && this.i !== 0 && this.i !== this.points.length - 1) {
this.points.splice(this.i--, 1);
this._forceRebuildNextRender = true;
this.layerRenderer.render();
@@ -596,9 +687,9 @@ export default class SplineEditor2 {
this.originalPoints = null;
};
this.dragHandler = (d) => {
let adjustedX = this.vis.mouse().x / app.canvas.ds.scale;
let adjustedY = this.vis.mouse().y / app.canvas.ds.scale;
this.dragHandler = (d, mouseX, mouseY) => {
let adjustedX = (typeof mouseX === 'number') ? mouseX : (this.vis.mouse().x / app.canvas.ds.scale);
let adjustedY = (typeof mouseY === 'number') ? mouseY : (this.vis.mouse().y / app.canvas.ds.scale);
if (this.isRotatingAll && this.anchorPoint && this.originalPoints) {
const currentAngle = Math.atan2(adjustedY - this.anchorPoint.y, adjustedX - this.anchorPoint.x);
@@ -677,7 +768,7 @@ export default class SplineEditor2 {
}
if (!this.isDraggingAll && !this.isScalingAll && !this.isRotatingAll) {
if (this.dragOffset) {
if (this.dragOffset && this.i >= 0 && this.points[this.i]) {
this.points[this.i].x = adjustedX + this.dragOffset.x;
this.points[this.i].y = adjustedY + this.dragOffset.y;
this.points[this.i].highlighted = !!this.points[this.i].highlighted;
@@ -687,7 +778,7 @@ export default class SplineEditor2 {
};
this.mouseOverHandler = (d) => {
this.hoverIndex = this.points.indexOf(d);
this.hoverIndex = this.resolvePointIndex(d);
// Avoid rebuilding the scene on hover; a simple re-render is sufficient
if (this.layerRenderer?.vis) this.layerRenderer.vis.render();
};