diff --git a/.gitignore b/.gitignore index ac4abbd..1584c81 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/web/power_spline_editor/layer_renderer.js b/web/power_spline_editor/layer_renderer.js index 7bb59dd..017baaa 100644 --- a/web/power_spline_editor/layer_renderer.js +++ b/web/power_spline_editor/layer_renderer.js @@ -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 diff --git a/web/power_spline_editor/power_spline_editor.js b/web/power_spline_editor/power_spline_editor.js index 788a8b2..945781d 100644 --- a/web/power_spline_editor/power_spline_editor.js +++ b/web/power_spline_editor/power_spline_editor.js @@ -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); } } diff --git a/web/power_spline_editor/spline_canvas.js b/web/power_spline_editor/spline_canvas.js index 7d3d09e..a86871e 100644 --- a/web/power_spline_editor/spline_canvas.js +++ b/web/power_spline_editor/spline_canvas.js @@ -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(); };