power spline fix + anim
This commit is contained in:
@@ -250,13 +250,6 @@ async function extractImageFromSourceNode(sourceNodeObj) {
|
||||
}
|
||||
|
||||
const sourceNode = sourceNodeObj.node;
|
||||
console.log(`Attempting to extract image from source node:`, {
|
||||
id: sourceNode.id,
|
||||
type: sourceNode.type,
|
||||
widgets: sourceNode.widgets ? sourceNode.widgets.map(w => ({name: w.name, type: w.type})) : [],
|
||||
properties: sourceNode.properties
|
||||
});
|
||||
|
||||
try {
|
||||
// Different node types store image data differently
|
||||
switch (sourceNode.type) {
|
||||
@@ -403,15 +396,6 @@ async function extractImageFromSourceNode(sourceNodeObj) {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: log all available information about the source node
|
||||
console.log(`Debug: No image found in source node ${sourceNode.type}. Available data:`, {
|
||||
id: sourceNode.id,
|
||||
type: sourceNode.type,
|
||||
widgets: sourceNode.widgets?.map(w => ({name: w.name, type: w.type, value: typeof w.value === 'object' ? JSON.stringify(w.value) : w.value})),
|
||||
properties: sourceNode.properties,
|
||||
outputs: sourceNode.outputs
|
||||
});
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Error extracting image from source node ${sourceNode.type}:`, error);
|
||||
|
||||
@@ -13,7 +13,7 @@ export class HandDrawLayerWidget extends RgthreeBaseWidget {
|
||||
this.hitAreas = {
|
||||
toggle: { bounds: [0,0], onDown: this.onToggleDown },
|
||||
name: { bounds: [0,0] },
|
||||
drawLabel: { bounds: [0,0] },
|
||||
drawLabel: { bounds: [0,0], onClick: this.onEditToggle },
|
||||
repeatDec: { bounds: [0,0], onClick: this.onRepeatDec },
|
||||
repeatVal: { bounds: [0,0], onClick: this.onRepeatClick },
|
||||
repeatInc: { bounds: [0,0], onClick: this.onRepeatInc },
|
||||
@@ -82,6 +82,11 @@ export class HandDrawLayerWidget extends RgthreeBaseWidget {
|
||||
this.hitAreas.toggle.bounds = drawTogglePart(ctx, { posX, posY, height, value: this.value.on });
|
||||
posX += this.hitAreas.toggle.bounds[1] + innerMargin;
|
||||
|
||||
// Match normal layers: dim contents when layer is off
|
||||
if (!this.value.on) {
|
||||
ctx.globalAlpha = app.canvas.editor_alpha * 0.4;
|
||||
}
|
||||
|
||||
// Name
|
||||
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
|
||||
ctx.textAlign = 'left';
|
||||
@@ -101,11 +106,33 @@ export class HandDrawLayerWidget extends RgthreeBaseWidget {
|
||||
this.hitAreas.repeatAny.bounds = [repeatL[0], repeatR[0] + repeatR[1] - repeatL[0]];
|
||||
rposX -= drawNumberWidgetPart.WIDTH_TOTAL + 10;
|
||||
|
||||
// Draw label in the Interpolation slot (70px)
|
||||
// Replace the "draw" text with a centered edit icon button occupying the 70px slot
|
||||
const labelWidth = 70;
|
||||
// Armed only when editor is in per-layer edit mode for this widget
|
||||
const armed = (node?.editor?._handdrawMode === 'edit') && (node?.editor?._handdrawEditWidget === this);
|
||||
const slotX = rposX - labelWidth; // left edge of 70px slot
|
||||
const slotY = posY + (height - (height * 0.8)) / 2; // slight vertical inset
|
||||
const slotH = height * 0.8;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = armed ? '#0d3b4a' : LiteGraph.WIDGET_BGCOLOR;
|
||||
ctx.strokeStyle = armed ? '#2cc6ff' : LiteGraph.WIDGET_OUTLINE_COLOR;
|
||||
ctx.lineWidth = armed ? 2 : 1;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(slotX, slotY, labelWidth, slotH, [6]);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
// Pencil glyph centered in slot
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('draw', rposX - labelWidth / 2, midY);
|
||||
this.hitAreas.drawLabel.bounds = [rposX - labelWidth, labelWidth];
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
|
||||
ctx.font = `${Math.max(11, Math.min(16, slotH * 0.7))}px monospace`;
|
||||
ctx.fillText('✏️', slotX + labelWidth / 2, slotY + slotH / 2 + 0.5);
|
||||
ctx.restore();
|
||||
// Entire 70px area is clickable to toggle edit mode
|
||||
this.hitAreas.drawLabel.bounds = [slotX, labelWidth];
|
||||
|
||||
// Advance position accounting only for the slot width
|
||||
rposX -= labelWidth + 10;
|
||||
|
||||
// Z-Pause
|
||||
@@ -170,15 +197,45 @@ export class HandDrawLayerWidget extends RgthreeBaseWidget {
|
||||
ctx.textAlign = 'left';
|
||||
const nameText = fitString(ctx, this.value.name || 'Handdraw', rposX - posX - innerMargin);
|
||||
ctx.fillText(nameText, posX, midY);
|
||||
this.hitAreas.name.bounds = [posX, 0, rposX - posX, height];
|
||||
// Make name hit area wide and simple (x + width only) to avoid y-mismatch
|
||||
this.hitAreas.name.bounds = [posX, rposX - posX];
|
||||
// Activate layer when clicking the name area
|
||||
this.hitAreas.name.onDown = (event, pos, node) => {
|
||||
if (!node.layerManager) return false;
|
||||
const current = node.layerManager.getActiveWidget?.();
|
||||
if (current === this) {
|
||||
node.layerManager.setActiveWidget(null);
|
||||
} else {
|
||||
node.layerManager.setActiveWidget(this);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
this.hitAreas.name.onClick = (event, pos, node) => true; // handled onDown
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
onMouseDown(event, pos, node) {
|
||||
// Selecting the row should activate this layer
|
||||
onEditToggle(event, pos, node) {
|
||||
// Ensure this layer is active
|
||||
if (node.layerManager) {
|
||||
node.layerManager.setActiveWidget(this);
|
||||
}
|
||||
// Toggle handdraw mode on the editor, mirroring top-row pencil
|
||||
if (node?.editor) {
|
||||
if (node.editor._handdrawMode === 'edit' && node.editor._handdrawEditWidget === this) {
|
||||
node.editor.exitHanddrawMode?.(false);
|
||||
} else {
|
||||
node.editor.enterHanddrawMode?.('edit', this);
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
// Re-render canvas overlays
|
||||
try { node.editor.layerRenderer?.render(); } catch {}
|
||||
}
|
||||
this.cancelMouseDown?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
onMouseDown(event, pos, node) {
|
||||
// Activation is handled via hitAreas.name.onDown; do not duplicate here
|
||||
return super.onMouseDown?.(event, pos, node);
|
||||
}
|
||||
|
||||
@@ -294,20 +351,27 @@ export function commitHanddrawPath(node, points) {
|
||||
const lm = node.layerManager;
|
||||
if (!lm) return;
|
||||
const active = lm.getActiveWidget?.();
|
||||
const mode = node?.editor?._handdrawMode || 'off';
|
||||
const editWidget = node?.editor?._handdrawEditWidget || null;
|
||||
const isHand = active && active.value?.type === 'handdraw';
|
||||
const pointsStore = JSON.stringify(points || []);
|
||||
if (isHand) {
|
||||
active.value.points_store = pointsStore;
|
||||
if (mode === 'edit' && editWidget) {
|
||||
// Replace points in the targeted handdraw layer
|
||||
editWidget.value.points_store = pointsStore;
|
||||
// Avoid disarming edit mode due to active-layer refresh
|
||||
if (node?.editor) node.editor._suppressHanddrawExitOnce = true;
|
||||
lm.setActiveWidget(editWidget);
|
||||
node.setDirtyCanvas(true, true);
|
||||
if (node.editor?.layerRenderer) node.editor.layerRenderer.render();
|
||||
if (node.editor?.updatePath) node.editor.updatePath();
|
||||
return active;
|
||||
return editWidget;
|
||||
}
|
||||
// create new handdraw layer
|
||||
// In 'create' mode, or fallback: create a new handdraw layer for each stroke
|
||||
if (lm.addNewHanddraw) {
|
||||
const w = lm.addNewHanddraw('Handdraw');
|
||||
const w = lm.addNewHanddraw('Handdraw', /*activate*/ false);
|
||||
w.value.points_store = pointsStore;
|
||||
// Make the new handdraw layer active
|
||||
// Suppress one-shot exit of create mode during programmatic switch
|
||||
if (node?.editor) node.editor._suppressHanddrawExitOnce = true;
|
||||
lm.setActiveWidget(w);
|
||||
node.setDirtyCanvas(true, true);
|
||||
if (node.editor?.layerRenderer) node.editor.layerRenderer.render();
|
||||
|
||||
@@ -18,27 +18,228 @@ export class LayerRenderer {
|
||||
// Store metadata for inactive layers to enable hit detection
|
||||
// Format: [{ widget: widgetRef, points: [...], panel: panelRef }, ...]
|
||||
this.inactiveLayerMetadata = [];
|
||||
|
||||
// Flow animation for inactive layers (non-"points" types)
|
||||
this._dashAnimOffset = 0;
|
||||
this._lastDashUpdateMs = 0;
|
||||
this._dashTimer = setInterval(() => {
|
||||
this._dashAnimOffset = (this._dashAnimOffset + 2) % 1000;
|
||||
try { this.updateInactiveDash(); } catch {}
|
||||
}, 80);
|
||||
}
|
||||
|
||||
// Apply/update marching-dash on inactive paths via DOM attributes
|
||||
updateInactiveDash() {
|
||||
if (!this.splineEditor) return;
|
||||
const svg = this.vis && this.vis.canvas ? this.vis.canvas() : null;
|
||||
if (!svg) return;
|
||||
// If disabled, strip dash styling and stop
|
||||
if (this.splineEditor._inactiveFlowEnabled === false) {
|
||||
const allPaths = svg.getElementsByTagName('path');
|
||||
for (const p of allPaths) {
|
||||
if (p.hasAttribute('stroke-dasharray')) p.removeAttribute('stroke-dasharray');
|
||||
if (p.hasAttribute('stroke-dashoffset')) p.removeAttribute('stroke-dashoffset');
|
||||
if (p.dataset && 'dashOffset' in p.dataset) delete p.dataset.dashOffset;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const paths = svg.getElementsByTagName('path');
|
||||
// If any target path lacks a dasharray, force an immediate apply (skip throttle)
|
||||
let needsInitialApply = false;
|
||||
for (const el of paths) {
|
||||
if (el.getAttribute) {
|
||||
const stroke = (el.getAttribute('stroke') || '').toLowerCase();
|
||||
const sw = Number(el.getAttribute('stroke-width') || '0');
|
||||
if ((stroke.includes('120,70,180') || stroke.includes('#7846b4') || stroke.includes('255,127,14') || stroke.includes('#ff7f0e')) && sw > 1 && !el.hasAttribute('stroke-dasharray')) {
|
||||
needsInitialApply = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Throttle dash updates to reduce load during active drawing
|
||||
const now = Date.now();
|
||||
const minInterval = this.splineEditor._handdrawActive ? 120 : 60; // slower while drawing
|
||||
if (!needsInitialApply && (now - this._lastDashUpdateMs < minInterval)) return;
|
||||
this._lastDashUpdateMs = now;
|
||||
const norm = (s) => (s || '').replace(/\s+/g, '').toLowerCase();
|
||||
const isInactiveTarget = (p) => {
|
||||
const stroke = norm(p.getAttribute('stroke'));
|
||||
const blue = '#1f77b4'; // active normal
|
||||
const yellow = '#d7c400'; // active handdraw
|
||||
if (stroke === blue || stroke === '#1f77b4' || stroke === yellow || stroke === '#d7c400') return false;
|
||||
// Orange inactive (rgba or rgb) or purple inactive
|
||||
const isOrange = stroke.includes('255,127,14') || stroke.includes('#ff7f0e') || stroke.includes('rgba(255,127,14') || stroke.includes('rgb(255,127,14)');
|
||||
const isPurple = stroke.includes('120,70,180') || stroke.includes('#7846b4') || stroke.includes('rgb(120,70,180)');
|
||||
if (!(isOrange || isPurple)) return false;
|
||||
// Exclude thin 1px orange (points type) by stroke-width
|
||||
const sw = Number(p.getAttribute('stroke-width') || '0');
|
||||
if (isOrange && sw <= 1.01) return false;
|
||||
return true;
|
||||
};
|
||||
// Helper easing functions (return 0..1 shape factor)
|
||||
const easeValue = (t, mode) => {
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
switch (mode) {
|
||||
case 'in': return t; // grow along path
|
||||
case 'out': return 1 - t; // shrink along path
|
||||
case 'in_out': return Math.sin(Math.PI * t); // squeezed at ends, widest in middle
|
||||
case 'out_in': return 1 - Math.sin(Math.PI * t); // widest at ends, squeezed middle
|
||||
case 'linear':
|
||||
default: return 1; // constant strip size
|
||||
}
|
||||
};
|
||||
// Split candidates by color to preserve draw order mapping
|
||||
const phase = ((this._dashAnimOffset % 200) / 200);
|
||||
const purplePaths = [];
|
||||
const orangePaths = [];
|
||||
for (const p of paths) {
|
||||
if (!isInactiveTarget(p)) continue;
|
||||
const stroke = norm(p.getAttribute('stroke'));
|
||||
const isPurple = stroke.includes('120,70,180') || stroke.includes('#7846b4') || stroke.includes('rgb(120,70,180)');
|
||||
const list = isPurple ? purplePaths : orangePaths;
|
||||
const len = (typeof p.getTotalLength === 'function') ? p.getTotalLength() : 0;
|
||||
list.push({ p, len });
|
||||
}
|
||||
const purpleMetas = (this.inactiveLayerMetadata || []).filter(d => d?.widget?.value?.type === 'handdraw');
|
||||
const orangeMetas = (this.inactiveLayerMetadata || []).filter(d => (d?.widget?.value?.type !== 'handdraw') && ((d?.widget?.value?.interpolation || 'linear') !== 'points'));
|
||||
const applyFor = (pairs, metas) => {
|
||||
const count = Math.min(pairs.length, metas.length);
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
const { p, len: pathLength } = pairs[idx];
|
||||
const easingMode = metas[idx]?.widget?.value?.easing || 'linear';
|
||||
const segments = Math.max(14, Math.min(100, Math.round(pathLength / 36)));
|
||||
const baseDash = 10;
|
||||
const baseGap = 6;
|
||||
const minFactor = 0.25;
|
||||
const maxFactor = 1.6;
|
||||
const pattern = [];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const t = (i / segments + phase) % 1;
|
||||
const f = easeValue(t, easingMode);
|
||||
const dash = Math.max(2, baseDash * (minFactor + (maxFactor - minFactor) * f));
|
||||
const gap = Math.max(2, baseGap * (1.0 - 0.2 * f));
|
||||
pattern.push(dash, gap);
|
||||
sum += dash + gap;
|
||||
}
|
||||
if (sum > 0 && pathLength > 0) {
|
||||
const scale = pathLength / sum;
|
||||
for (let k = 0; k < pattern.length; k++) pattern[k] = Math.max(1, pattern[k] * scale);
|
||||
}
|
||||
p.setAttribute('stroke-dasharray', pattern.join(' '));
|
||||
const dashOffsetPx = -phase * (pathLength || 1);
|
||||
p.setAttribute('stroke-dashoffset', String(dashOffsetPx));
|
||||
}
|
||||
};
|
||||
applyFor(purplePaths, purpleMetas);
|
||||
applyFor(orangePaths, orangeMetas);
|
||||
}
|
||||
|
||||
// Apply/update easing-shaped dash for ACTIVE handdraw layer (yellow line)
|
||||
updateActiveHanddrawDash() {
|
||||
if (!this.splineEditor || this.splineEditor._inactiveFlowEnabled === false) return;
|
||||
const now = Date.now();
|
||||
const minInterval = this.splineEditor._handdrawActive ? 120 : 60;
|
||||
if ((this._lastDashUpdateMs || 0) && (now - this._lastDashUpdateMs < minInterval)) return;
|
||||
this._lastDashUpdateMs = now;
|
||||
|
||||
const svg = this.vis && this.vis.canvas ? this.vis.canvas() : null;
|
||||
if (!svg) return;
|
||||
const active = this.node?.layerManager?.getActiveWidget?.();
|
||||
if (!active || active.value?.type !== 'handdraw') return;
|
||||
|
||||
const easingMode = active.value?.easing || 'linear';
|
||||
const paths = svg.getElementsByTagName('path');
|
||||
const norm = (s) => (s || '').replace(/\s+/g, '').toLowerCase();
|
||||
const yellow = '#d7c400';
|
||||
|
||||
// Easing factor
|
||||
const easeValue = (t, mode) => {
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
switch (mode) {
|
||||
case 'in': return t;
|
||||
case 'out': return 1 - t;
|
||||
case 'in_out': return Math.sin(Math.PI * t);
|
||||
case 'out_in': return 1 - Math.sin(Math.PI * t);
|
||||
case 'linear':
|
||||
default: return 1;
|
||||
}
|
||||
};
|
||||
|
||||
const phase = ((this._dashAnimOffset % 200) / 200);
|
||||
|
||||
for (const p of paths) {
|
||||
const stroke = norm(p.getAttribute('stroke'));
|
||||
if (stroke !== yellow && stroke !== '#d7c400') continue;
|
||||
const pathLength = (typeof p.getTotalLength === 'function') ? p.getTotalLength() : 0;
|
||||
const segments = Math.max(14, Math.min(100, Math.round(pathLength / 36)));
|
||||
const baseDash = 10;
|
||||
const baseGap = 6;
|
||||
const minFactor = 0.35;
|
||||
const maxFactor = 1.45;
|
||||
const pattern = [];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const t = (i / segments + phase) % 1;
|
||||
const f = easeValue(t, easingMode);
|
||||
const dash = Math.max(2, baseDash * (minFactor + (maxFactor - minFactor) * f));
|
||||
const gap = baseGap;
|
||||
pattern.push(dash, gap);
|
||||
sum += dash + gap;
|
||||
}
|
||||
if (sum > 0 && pathLength > 0) {
|
||||
const scale = pathLength / sum;
|
||||
for (let k = 0; k < pattern.length; k++) pattern[k] = Math.max(1, pattern[k] * scale);
|
||||
}
|
||||
p.setAttribute('stroke-dasharray', pattern.join(' '));
|
||||
const dashOffsetPx = -phase * (pathLength || 1);
|
||||
p.setAttribute('stroke-dashoffset', String(dashOffsetPx));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main render method. Clears previous drawings and redraws all layers.
|
||||
*/
|
||||
render() {
|
||||
// During drag operations, avoid rebuilding the scene graph.
|
||||
// Recreating marks while Protovis drag behavior is active breaks internal state
|
||||
// and causes errors like reading undefined 'x'/'fix'. Just re-render in place.
|
||||
const forceRebuild = !!this.splineEditor._forceRebuildNextRender;
|
||||
if (forceRebuild) {
|
||||
this.splineEditor._forceRebuildNextRender = false;
|
||||
}
|
||||
if (!forceRebuild && (this.splineEditor.isDragging || this.splineEditor.isDraggingAll || this.splineEditor.isScalingAll || this.splineEditor.isRotatingAll)) {
|
||||
this.vis.render();
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearLayers();
|
||||
|
||||
const activeWidget = this.node.layerManager.getActiveWidget();
|
||||
const allWidgets = this.node.layerManager.getSplineWidgets();
|
||||
const isDrawing = !!this.splineEditor._handdrawActive;
|
||||
const handdrawMode = this.splineEditor._handdrawMode || 'off';
|
||||
const activeIsHand = !!(activeWidget && activeWidget.value && activeWidget.value.type === 'handdraw');
|
||||
|
||||
// Render inactive and off layers - each in its own isolated panel
|
||||
allWidgets.forEach(widget => {
|
||||
if (widget !== activeWidget) {
|
||||
// While drawing in 'create' mode, also render the currently selected layer as inactive
|
||||
const treatActiveAsInactive = isDrawing && handdrawMode === 'create' && widget === activeWidget;
|
||||
if (widget !== activeWidget || treatActiveAsInactive) {
|
||||
this.drawInactiveLayer(widget);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw the active layer on top in its dedicated panel
|
||||
if (activeWidget) {
|
||||
if (activeWidget && !(isDrawing && handdrawMode === 'create')) {
|
||||
// Normal case: draw the active widget unless we're creating a new handdraw stroke
|
||||
this.drawActiveLayer(activeWidget);
|
||||
}
|
||||
if (this.splineEditor._handdrawActive && this.splineEditor.points && this.splineEditor.points.length > 0) {
|
||||
// During handdraw drawing, render a live preview on top (even if another layer is selected)
|
||||
// No active widget yet (e.g., first-ever handdraw stroke). Render a live preview
|
||||
// using handdraw styling so users can see the stroke while drawing.
|
||||
const pseudoWidget = { value: { interpolation: 'linear', type: 'handdraw' } };
|
||||
this.drawActiveLayer(pseudoWidget);
|
||||
}
|
||||
|
||||
// Ensure active panel is always the last child so it renders on top
|
||||
@@ -51,6 +252,14 @@ export class LayerRenderer {
|
||||
|
||||
// Apply the changes to the SVG
|
||||
this.vis.render();
|
||||
// Avoid extra DOM work while freehand drawing; timer will animate
|
||||
if (!this.splineEditor._handdrawActive) {
|
||||
this.updateInactiveDash();
|
||||
this.updateActiveHanddrawDash();
|
||||
} else {
|
||||
// still update lazily by throttle
|
||||
this.updateActiveHanddrawDash();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,7 +357,11 @@ export class LayerRenderer {
|
||||
}
|
||||
return '#1f77b4';
|
||||
})
|
||||
.lineWidth(() => interpolation === 'points' ? 1 : 3);
|
||||
.lineWidth(() => {
|
||||
// Handdraw (active) should have same thickness as normal lines
|
||||
if (isHanddraw) return 3;
|
||||
return interpolation === 'points' ? 1 : 3;
|
||||
});
|
||||
|
||||
// ALWAYS draw dots - bigger for points mode, all circles
|
||||
// Use closures to ensure drag handlers can find points via indexOf()
|
||||
@@ -157,7 +370,7 @@ export class LayerRenderer {
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(() => {
|
||||
if (isHanddraw) return 4; // smaller points for handdraw
|
||||
if (isHanddraw) return 2; // make yellow hand-draw points smaller
|
||||
return interpolation === 'points' ? 9 : 6;
|
||||
})
|
||||
.shape((dot) => {
|
||||
@@ -213,12 +426,12 @@ export class LayerRenderer {
|
||||
drawInactiveLayer(widget) {
|
||||
const isOff = !widget.value.on;
|
||||
const isHanddraw = widget?.value?.type === 'handdraw';
|
||||
const strokeColor = isOff
|
||||
let strokeColor = isOff
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: (isHanddraw ? "rgba(70, 40, 110, 0.7)" : "rgba(255, 127, 14, 0.5)");
|
||||
: (isHanddraw ? "rgba(120, 70, 180, 0.85)" : "rgba(255, 127, 14, 0.5)");
|
||||
const fillColor = isOff
|
||||
? "rgba(255, 255, 255, 0.1)"
|
||||
: (isHanddraw ? "rgba(70, 40, 110, 0.5)" : "rgba(255, 127, 14, 0.4)");
|
||||
: (isHanddraw ? "rgba(120, 70, 180, 0.6)" : "rgba(255, 127, 14, 0.4)");
|
||||
|
||||
let points;
|
||||
try {
|
||||
@@ -243,6 +456,10 @@ export class LayerRenderer {
|
||||
});
|
||||
|
||||
const interpolation = widget.value.interpolation || 'linear';
|
||||
// For inactive points layers (non-handdraw), reduce line opacity to 20%
|
||||
if (!isHanddraw && !isOff && interpolation === 'points') {
|
||||
strokeColor = "rgba(255, 127, 14, 0.2)";
|
||||
}
|
||||
|
||||
// Snapshot points to avoid closure issues
|
||||
const pointsSnapshot = [...points];
|
||||
@@ -285,13 +502,14 @@ export class LayerRenderer {
|
||||
|
||||
// ALWAYS draw line
|
||||
const lineWidthMain = (interpolation === 'points') ? (isHanddraw ? 3 : 1) : 3;
|
||||
layerPanel.add(pv.Line).events("none")
|
||||
const inactiveLine = layerPanel.add(pv.Line).events("none")
|
||||
.data(renderPoints)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.interpolate(interpolation === 'points' ? 'linear' : interpolation)
|
||||
.strokeStyle(strokeColor)
|
||||
.lineWidth(lineWidthMain);
|
||||
// Note: dash animation is applied post-render via updateInactiveDash()
|
||||
|
||||
// If this is a handdraw layer in inactive (purple) state, render line-only and skip markers/dots
|
||||
if (isHanddraw) {
|
||||
@@ -308,8 +526,17 @@ export class LayerRenderer {
|
||||
.interpolate('linear')
|
||||
.strokeStyle(strokeColor)
|
||||
.lineWidth(lineWidthPts);
|
||||
// Do not draw circles for inactive layers. Optionally keep first-point triangle for non-handdraw.
|
||||
// For points-type layers, also show point markers when inactive (non-handdraw only)
|
||||
if (!isHanddraw && pointsSnapshot.length > 0) {
|
||||
layerPanel.add(pv.Dot).events("none")
|
||||
.data(pointsSnapshot)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(7.0)
|
||||
.shape('circle')
|
||||
.strokeStyle(strokeColor)
|
||||
.fillStyle(fillColor);
|
||||
// Keep first-point triangle marker for direction
|
||||
layerPanel.add(pv.Dot).events("none")
|
||||
.data([pointsSnapshot[0]])
|
||||
.left(d => d.x)
|
||||
|
||||
@@ -69,7 +69,7 @@ async function saveRefImageToCache(base64Data) {
|
||||
try {
|
||||
// Create a cache key to track the current ref_image
|
||||
const currentHash = await simpleHash(base64Data);
|
||||
sessionStorage.setItem('spline-editor-ref-image-hash', currentHash);
|
||||
safeSetSessionItem('spline-editor-ref-image-hash', currentHash);
|
||||
|
||||
// Try to save the file to the bg folder via the backend API
|
||||
try {
|
||||
@@ -86,7 +86,7 @@ async function saveRefImageToCache(base64Data) {
|
||||
if (result.success) {
|
||||
|
||||
// Save to sessionStorage as backup
|
||||
sessionStorage.setItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
safeSetSessionItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
base64: base64Data,
|
||||
type: 'image/png',
|
||||
name: 'ref_image.jpg',
|
||||
@@ -98,7 +98,7 @@ async function saveRefImageToCache(base64Data) {
|
||||
} else {
|
||||
console.error('Backend error saving ref image:', result.error);
|
||||
// Fallback to sessionStorage only
|
||||
sessionStorage.setItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
safeSetSessionItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
base64: base64Data,
|
||||
type: 'image/png',
|
||||
name: 'ref_image.jpg',
|
||||
@@ -110,7 +110,7 @@ async function saveRefImageToCache(base64Data) {
|
||||
} else {
|
||||
console.error('Failed to save ref image via API:', response.status);
|
||||
// Fallback to sessionStorage only
|
||||
sessionStorage.setItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
safeSetSessionItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
base64: base64Data,
|
||||
type: 'image/png',
|
||||
name: 'ref_image.jpg',
|
||||
@@ -123,7 +123,7 @@ async function saveRefImageToCache(base64Data) {
|
||||
console.warn('API save failed, using sessionStorage fallback:', error);
|
||||
|
||||
// Fallback to sessionStorage if API fails
|
||||
sessionStorage.setItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
safeSetSessionItem('spline-editor-cached-ref-image', JSON.stringify({
|
||||
base64: base64Data,
|
||||
type: 'image/png',
|
||||
name: 'ref_image.jpg',
|
||||
@@ -233,17 +233,27 @@ class SplineLayerManager {
|
||||
this.node = node;
|
||||
this.activeWidget = null;
|
||||
this.node.splineWidgetsCounter = 0;
|
||||
this._noActiveSelection = false;
|
||||
}
|
||||
|
||||
setActiveWidget(widget) {
|
||||
if (this.activeWidget === widget) {
|
||||
console.log("[SplineLayerManager] setActiveWidget: already active:", widget?.value?.name);
|
||||
return;
|
||||
}
|
||||
console.log("[SplineLayerManager] setActiveWidget: switching from", this.activeWidget?.value?.name, "to", widget?.value?.name);
|
||||
// Track explicit "no selection" state
|
||||
this._noActiveSelection = (widget == null);
|
||||
// If the editor is in handdraw create/edit mode, disarm it when switching layers
|
||||
// Exception: when switch is initiated by handdraw commit flow, one-shot suppress exit
|
||||
try {
|
||||
const ed = this.node?.editor;
|
||||
const suppressOnce = !!ed?._suppressHanddrawExitOnce;
|
||||
if (ed) ed._suppressHanddrawExitOnce = false; // always reset flag
|
||||
if (ed && ed._handdrawMode && ed._handdrawMode !== 'off' && !suppressOnce) {
|
||||
ed.exitHanddrawMode?.(false);
|
||||
}
|
||||
} catch {}
|
||||
this.activeWidget = widget;
|
||||
if (this.node.editor) {
|
||||
console.log("[SplineLayerManager] setActiveWidget: notifying editor");
|
||||
this.node.editor.onActiveLayerChanged(); // Notify canvas
|
||||
}
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
@@ -254,6 +264,7 @@ class SplineLayerManager {
|
||||
this.activeWidget = null;
|
||||
}
|
||||
if (!this.activeWidget) {
|
||||
if (this._noActiveSelection) return null;
|
||||
const splines = this.getSplineWidgets();
|
||||
if (splines.length > 0) {
|
||||
// Find the first visible/active spline widget, not just any spline
|
||||
@@ -331,7 +342,7 @@ class SplineLayerManager {
|
||||
return widget;
|
||||
}
|
||||
|
||||
addNewHanddraw(name) {
|
||||
addNewHanddraw(name, activate = true) {
|
||||
this.node.splineWidgetsCounter++;
|
||||
const widget = new HandDrawLayerWidget("spline_" + this.node.splineWidgetsCounter);
|
||||
|
||||
@@ -357,11 +368,13 @@ class SplineLayerManager {
|
||||
}
|
||||
|
||||
this.node.updateNodeHeight();
|
||||
this.setActiveWidget(widget);
|
||||
if (this.node.editor && this.node.editor.vis) {
|
||||
this.node.editor.vis.render();
|
||||
if (activate) {
|
||||
this.setActiveWidget(widget);
|
||||
if (this.node.editor && this.node.editor.vis) {
|
||||
this.node.editor.vis.render();
|
||||
}
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
}
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
return widget;
|
||||
}
|
||||
|
||||
@@ -624,6 +637,11 @@ class SplineLayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Safe sessionStorage setter to avoid quota errors
|
||||
function safeSetSessionItem(key, value) {
|
||||
try { sessionStorage.setItem(key, value); } catch (e) { /* ignore quota/unsupported */ }
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'WanVideoWrapper_QQ.PowerSplineEditor',
|
||||
|
||||
@@ -826,19 +844,19 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
this.hasSplineWidgets = function() {
|
||||
return this.widgets && this.widgets.some(w => w instanceof PowerSplineWidget);
|
||||
return this.widgets && this.widgets.some(w => (w instanceof PowerSplineWidget) || (w instanceof HandDrawLayerWidget));
|
||||
};
|
||||
|
||||
this.allSplinesState = function() {
|
||||
const splineWidgets = this.widgets.filter(w => w instanceof PowerSplineWidget);
|
||||
if (!splineWidgets.length) return false;
|
||||
return splineWidgets.every(w => w.value.on);
|
||||
const layerWidgets = this.widgets.filter(w => (w instanceof PowerSplineWidget) || (w instanceof HandDrawLayerWidget));
|
||||
if (!layerWidgets.length) return false;
|
||||
return layerWidgets.every(w => w.value.on);
|
||||
};
|
||||
|
||||
this.toggleAllSplines = function() {
|
||||
const splineWidgets = this.widgets.filter(w => w instanceof PowerSplineWidget);
|
||||
const layerWidgets = this.widgets.filter(w => (w instanceof PowerSplineWidget) || (w instanceof HandDrawLayerWidget));
|
||||
const newState = !this.allSplinesState();
|
||||
splineWidgets.forEach(w => w.value.on = newState);
|
||||
layerWidgets.forEach(w => w.value.on = newState);
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
@@ -1192,8 +1210,6 @@ app.registerExtension({
|
||||
alert('Could not retrieve reference image from connected node. Make sure an image node is connected to the ref_image input.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Successfully retrieved reference image from connected node, updating...');
|
||||
|
||||
// Store the original reference image separately to use as base for overlays
|
||||
this.originalRefImageData = {
|
||||
@@ -1222,7 +1238,6 @@ app.registerExtension({
|
||||
this.editor.refreshBackgroundImage();
|
||||
}
|
||||
|
||||
console.log('Reference image updated successfully from connected node');
|
||||
} catch (error) {
|
||||
console.error('Error updating reference image from connected node:', error);
|
||||
alert('Error updating reference image: ' + error.message);
|
||||
@@ -1818,7 +1833,7 @@ app.registerExtension({
|
||||
|
||||
// Also save dimensions to sessionStorage for persistence across page refreshes
|
||||
try {
|
||||
sessionStorage.setItem(`spline-editor-dims-${this.uuid}`, JSON.stringify({
|
||||
safeSetSessionItem(`spline-editor-dims-${this.uuid}`, JSON.stringify({
|
||||
width: dims.width,
|
||||
height: dims.height
|
||||
}));
|
||||
@@ -1837,7 +1852,7 @@ app.registerExtension({
|
||||
try {
|
||||
const size = JSON.stringify(this.imgData).length;
|
||||
if (size < 640 * 480) { // 1MB limit
|
||||
sessionStorage.setItem(`spline-editor-img-${this.uuid}`, JSON.stringify(this.imgData));
|
||||
safeSetSessionItem(`spline-editor-img-${this.uuid}`, JSON.stringify(this.imgData));
|
||||
} else {
|
||||
console.warn("Spline Editor: Image not saved to session storage because it is too large.", size);
|
||||
}
|
||||
|
||||
@@ -59,17 +59,52 @@ export default class SplineEditor2 {
|
||||
|
||||
// Get interpolation from active widget
|
||||
this.interpolation = 'linear';
|
||||
// Handdraw mode (UI/UX first pass)
|
||||
this._handdrawActive = false;
|
||||
// Handdraw modes: 'off' | 'create' | 'edit'
|
||||
this._handdrawMode = 'off';
|
||||
this._handdrawActive = false; // legacy flag used by renderer
|
||||
this._handdrawEditWidget = null;
|
||||
this._handdrawPath = [];
|
||||
this.enterHanddrawMode = () => {
|
||||
// Global toggle for inactive flow animation (default ON)
|
||||
this._inactiveFlowEnabled = true;
|
||||
this.enterHanddrawMode = (mode = 'create', widget = null) => {
|
||||
this._handdrawMode = mode;
|
||||
this._handdrawEditWidget = mode === 'edit' ? widget : null;
|
||||
this._handdrawActive = true;
|
||||
this._handdrawPath = [];
|
||||
|
||||
// Install right-click handler to exit edit mode reliably
|
||||
if (this._teardownHanddrawListeners) {
|
||||
try { this._teardownHanddrawListeners(); } catch {}
|
||||
this._teardownHanddrawListeners = null;
|
||||
}
|
||||
const canvasEl = this.vis?.canvas?.();
|
||||
if ((mode === 'edit' || mode === 'create') && canvasEl) {
|
||||
const onMouseDown = (ev) => {
|
||||
// Middle click exits handdraw modes (edit/create), preserving current curve
|
||||
if (ev && ev.button === 1) {
|
||||
ev.preventDefault?.(); ev.stopPropagation?.();
|
||||
try { this.exitHanddrawMode(false); } catch {}
|
||||
try { this.layerRenderer?.render(); } catch {}
|
||||
try { this.node?.setDirtyCanvas?.(true, true); } catch {}
|
||||
}
|
||||
};
|
||||
canvasEl.addEventListener('mousedown', onMouseDown, true);
|
||||
this._teardownHanddrawListeners = () => {
|
||||
canvasEl.removeEventListener('mousedown', onMouseDown, true);
|
||||
};
|
||||
}
|
||||
};
|
||||
this.exitHanddrawMode = (commit = false) => {
|
||||
// Only cancel UI state; no commit normalization here (handled in mousedown mouseup flow)
|
||||
// Only cancel UI state; no commit normalization here (handled in mousedown/mouseup flow)
|
||||
this._handdrawMode = 'off';
|
||||
this._handdrawEditWidget = null;
|
||||
this._handdrawActive = false;
|
||||
this._handdrawPath = [];
|
||||
// Remove temporary listeners
|
||||
if (this._teardownHanddrawListeners) {
|
||||
try { this._teardownHanddrawListeners(); } catch {}
|
||||
this._teardownHanddrawListeners = null;
|
||||
}
|
||||
// Re-render to update any UI state
|
||||
try { this.layerRenderer?.render(); } catch {}
|
||||
};
|
||||
@@ -333,8 +368,9 @@ export default class SplineEditor2 {
|
||||
// Normalize and commit
|
||||
const norm = this.normalizePoints(simplified);
|
||||
try { this.node?.commitHanddraw?.(norm); } catch {}
|
||||
this._handdrawActive = false;
|
||||
// After committing, remain in current mode; clear temporary path
|
||||
this._handdrawPath = [];
|
||||
this._handdrawActive = (this._handdrawMode !== 'off');
|
||||
// Reload active points from widget and re-render
|
||||
this.points = this.getActivePoints();
|
||||
this.layerRenderer.render();
|
||||
@@ -408,10 +444,19 @@ export default class SplineEditor2 {
|
||||
this.updatePath();
|
||||
}
|
||||
else if (pv.event.button === 2) {
|
||||
// Open custom canvas context menu and suppress native one
|
||||
// Right-click behavior: open custom canvas context menu and suppress native one
|
||||
try { pv.event.preventDefault?.(); pv.event.stopPropagation?.(); } catch {}
|
||||
|
||||
const menuEl = this.node.contextMenu;
|
||||
// Update first menu item label based on active layer type
|
||||
try {
|
||||
const activeWidget = this.getActiveWidget?.();
|
||||
const isHanddraw = !!(activeWidget && activeWidget.value && activeWidget.value.type === 'handdraw');
|
||||
const firstItem = this.node.menuItems && this.node.menuItems[0];
|
||||
if (firstItem) {
|
||||
firstItem.textContent = isHanddraw ? 'Edit' : 'Invert point order';
|
||||
}
|
||||
} catch {}
|
||||
menuEl.style.display = 'block';
|
||||
menuEl.style.left = `${pv.event.clientX}px`;
|
||||
menuEl.style.top = `${pv.event.clientY}px`;
|
||||
@@ -445,6 +490,13 @@ export default class SplineEditor2 {
|
||||
document.addEventListener('keydown', onEsc, true);
|
||||
}, 0);
|
||||
}
|
||||
// Middle click: exit any handdraw mode (edit/create) and keep current edits
|
||||
else if (pv.event.button === 1) {
|
||||
try { this.exitHanddrawMode(false); } catch {}
|
||||
try { this.layerRenderer?.render(); } catch {}
|
||||
try { this.node?.setDirtyCanvas?.(true, true); } catch {}
|
||||
return this;
|
||||
}
|
||||
})
|
||||
|
||||
this.dragStartHandler = (d) => {
|
||||
@@ -479,10 +531,21 @@ export default class SplineEditor2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Alt+Middle-Click: Translation (any point)
|
||||
// Alt + Middle-Click: Translation (any point)
|
||||
if (pv.event.altKey && pv.event.button === 1) {
|
||||
this.isDraggingAll = true;
|
||||
this.dragStartPos = { x: mouseX, y: mouseY };
|
||||
// Snapshot original positions to keep a rigid translation anchored at clicked point
|
||||
this.translateAllSnapshot = {
|
||||
points: this.points.map(p => ({ ...p })),
|
||||
pivotIndex: this.i,
|
||||
pivotX: this.points[this.i].x,
|
||||
pivotY: this.points[this.i].y,
|
||||
offsetX: this.dragOffset ? this.dragOffset.x : 0,
|
||||
offsetY: this.dragOffset ? this.dragOffset.y : 0,
|
||||
};
|
||||
// Provide a neutral fix object so Protovis drag doesn't apply extra offsets
|
||||
try { if (dot && typeof dot === 'object') { dot.fix = { x: 0, y: 0 }; } } catch {}
|
||||
this.isDragging = true;
|
||||
return;
|
||||
}
|
||||
@@ -505,6 +568,7 @@ export default class SplineEditor2 {
|
||||
this.isDragging = true;
|
||||
if (pv.event.button === 2 && this.i !== 0 && this.i !== this.points.length - 1) {
|
||||
this.points.splice(this.i--, 1);
|
||||
this._forceRebuildNextRender = true;
|
||||
this.layerRenderer.render();
|
||||
}
|
||||
};
|
||||
@@ -516,6 +580,9 @@ export default class SplineEditor2 {
|
||||
if (this.pathElements !== null) {
|
||||
this.updatePath();
|
||||
}
|
||||
// Clear any protovis drag fix offsets left on data objects
|
||||
try { if (Array.isArray(this.points)) { for (const p of this.points) { if (p && p.fix !== undefined) delete p.fix; } } } catch {}
|
||||
this.translateAllSnapshot = null;
|
||||
this.dragOffset = null;
|
||||
this.isDragging = false;
|
||||
this.isDraggingAll = false;
|
||||
@@ -541,20 +608,20 @@ export default class SplineEditor2 {
|
||||
|
||||
// Rotate all points around the anchor point
|
||||
for (let j = 0; j < this.originalPoints.length; j++) {
|
||||
const originalPoint = this.originalPoints[j];
|
||||
if (j === this.anchorIndex) {
|
||||
// Keep anchor point fixed
|
||||
this.points[j] = { ...this.anchorPoint };
|
||||
this.points[j].x = this.anchorPoint.x;
|
||||
this.points[j].y = this.anchorPoint.y;
|
||||
this.points[j].highlighted = !!originalPoint.highlighted;
|
||||
} else {
|
||||
const originalPoint = this.originalPoints[j];
|
||||
const vecX = originalPoint.x - this.anchorPoint.x;
|
||||
const vecY = originalPoint.y - this.anchorPoint.y;
|
||||
const rotatedX = vecX * cos - vecY * sin;
|
||||
const rotatedY = vecX * sin + vecY * cos;
|
||||
this.points[j] = {
|
||||
x: this.anchorPoint.x + rotatedX,
|
||||
y: this.anchorPoint.y + rotatedY,
|
||||
highlighted: originalPoint.highlighted || false
|
||||
};
|
||||
this.points[j].x = this.anchorPoint.x + rotatedX;
|
||||
this.points[j].y = this.anchorPoint.y + rotatedY;
|
||||
this.points[j].highlighted = !!originalPoint.highlighted;
|
||||
}
|
||||
}
|
||||
this.layerRenderer.render();
|
||||
@@ -562,15 +629,22 @@ export default class SplineEditor2 {
|
||||
}
|
||||
|
||||
if (this.isDraggingAll && this.dragStartPos) {
|
||||
const deltaX = adjustedX - this.dragStartPos.x;
|
||||
const deltaY = adjustedY - this.dragStartPos.y;
|
||||
this.points = this.points.map(point => ({
|
||||
x: point.x + deltaX,
|
||||
y: point.y + deltaY,
|
||||
highlighted: point.highlighted || false
|
||||
}));
|
||||
this.dragStartPos.x = adjustedX;
|
||||
this.dragStartPos.y = adjustedY;
|
||||
// Rigid translation anchored at originally clicked point using snapshot
|
||||
const snap = this.translateAllSnapshot;
|
||||
const desiredX = adjustedX + (snap ? snap.offsetX : (this.dragOffset ? this.dragOffset.x : 0));
|
||||
const desiredY = adjustedY + (snap ? snap.offsetY : (this.dragOffset ? this.dragOffset.y : 0));
|
||||
const basePivotX = snap ? snap.pivotX : this.dragStartPos.x;
|
||||
const basePivotY = snap ? snap.pivotY : this.dragStartPos.y;
|
||||
const deltaX = desiredX - basePivotX;
|
||||
const deltaY = desiredY - basePivotY;
|
||||
try { if (d && typeof d === 'object') { d.fix = { x: 0, y: 0 }; } } catch {}
|
||||
const basePoints = snap ? snap.points : this.points;
|
||||
for (let j = 0; j < this.points.length; j++) {
|
||||
const bp = basePoints[j] || this.points[j];
|
||||
this.points[j].x = bp.x + deltaX;
|
||||
this.points[j].y = bp.y + deltaY;
|
||||
this.points[j].highlighted = !!this.points[j].highlighted;
|
||||
}
|
||||
this.layerRenderer.render();
|
||||
return;
|
||||
}
|
||||
@@ -584,18 +658,18 @@ export default class SplineEditor2 {
|
||||
|
||||
// Scale all points relative to the anchor point
|
||||
for (let j = 0; j < this.originalPoints.length; j++) {
|
||||
const originalPoint = this.originalPoints[j];
|
||||
if (j === this.anchorIndex) {
|
||||
// Keep anchor point fixed
|
||||
this.points[j] = { ...this.anchorPoint };
|
||||
this.points[j].x = this.anchorPoint.x;
|
||||
this.points[j].y = this.anchorPoint.y;
|
||||
this.points[j].highlighted = !!originalPoint.highlighted;
|
||||
} else {
|
||||
const originalPoint = this.originalPoints[j];
|
||||
const vecX = originalPoint.x - this.anchorPoint.x;
|
||||
const vecY = originalPoint.y - this.anchorPoint.y;
|
||||
this.points[j] = {
|
||||
x: this.anchorPoint.x + vecX * clampedScaleFactor,
|
||||
y: this.anchorPoint.y + vecY * clampedScaleFactor,
|
||||
highlighted: originalPoint.highlighted || false
|
||||
};
|
||||
this.points[j].x = this.anchorPoint.x + vecX * clampedScaleFactor;
|
||||
this.points[j].y = this.anchorPoint.y + vecY * clampedScaleFactor;
|
||||
this.points[j].highlighted = !!originalPoint.highlighted;
|
||||
}
|
||||
}
|
||||
this.layerRenderer.render();
|
||||
@@ -604,11 +678,9 @@ export default class SplineEditor2 {
|
||||
|
||||
if (!this.isDraggingAll && !this.isScalingAll && !this.isRotatingAll) {
|
||||
if (this.dragOffset) {
|
||||
this.points[this.i] = {
|
||||
x: adjustedX + this.dragOffset.x,
|
||||
y: adjustedY + this.dragOffset.y,
|
||||
highlighted: this.points[this.i].highlighted || false
|
||||
};
|
||||
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;
|
||||
}
|
||||
this.layerRenderer.render();
|
||||
}
|
||||
@@ -616,14 +688,16 @@ export default class SplineEditor2 {
|
||||
|
||||
this.mouseOverHandler = (d) => {
|
||||
this.hoverIndex = this.points.indexOf(d);
|
||||
this.layerRenderer.render();
|
||||
// Avoid rebuilding the scene on hover; a simple re-render is sufficient
|
||||
if (this.layerRenderer?.vis) this.layerRenderer.vis.render();
|
||||
};
|
||||
|
||||
this.mouseOutHandler = () => {
|
||||
if (!this.isDragging) {
|
||||
this.hoverIndex = -1;
|
||||
}
|
||||
this.layerRenderer.render();
|
||||
// Avoid rebuilding the scene on hover out
|
||||
if (this.layerRenderer?.vis) this.layerRenderer.vis.render();
|
||||
};
|
||||
|
||||
this.backgroundImage = this.vis.add(pv.Image).visible(false)
|
||||
@@ -1056,10 +1130,19 @@ export default class SplineEditor2 {
|
||||
document.addEventListener('contextmenu', hideOpenMenus, true);
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Invert point order
|
||||
// Context action depends on active layer type
|
||||
e.preventDefault();
|
||||
this.points.reverse();
|
||||
this.updatePath();
|
||||
const aw = this.getActiveWidget?.();
|
||||
const isHand = !!(aw && aw.value && aw.value.type === 'handdraw');
|
||||
if (isHand) {
|
||||
// Enter edit mode for the active handdraw layer
|
||||
try { this.enterHanddrawMode('edit', aw); } catch {}
|
||||
try { this.layerRenderer?.render(); } catch {}
|
||||
} else {
|
||||
// Invert point order for non-handdraw layers
|
||||
this.points.reverse();
|
||||
this.updatePath();
|
||||
}
|
||||
this.node.contextMenu.style.display = 'none';
|
||||
break;
|
||||
case 1:
|
||||
|
||||
@@ -153,7 +153,13 @@ export class TopRowWidget extends RgthreeBaseWidget {
|
||||
const refreshButtonWidth = availableWidth * 0.15;
|
||||
const bgImgDropdownWidth = availableWidth * 0.15;
|
||||
const drawButtonWidth = Math.max(20, Math.min(28, availableWidth * 0.05));
|
||||
const dimensionsAreaWidth = availableWidth - (refreshButtonWidth + spacing + bgImgDropdownWidth + spacing + drawButtonWidth + spacing);
|
||||
// Account for both the pencil button and the animation toggle button
|
||||
const dimensionsAreaWidth = availableWidth - (
|
||||
refreshButtonWidth + spacing +
|
||||
bgImgDropdownWidth + spacing +
|
||||
drawButtonWidth + spacing + // ✏️
|
||||
drawButtonWidth + spacing // 〰
|
||||
);
|
||||
|
||||
// Calculate total width and starting position to center everything
|
||||
const totalWidth = refreshButtonWidth + spacing + bgImgDropdownWidth + spacing + dimensionsAreaWidth;
|
||||
@@ -199,7 +205,8 @@ export class TopRowWidget extends RgthreeBaseWidget {
|
||||
posX += bgImgDropdownWidth + spacing;
|
||||
|
||||
// Small Draw icon button between overlay picker and dimensions
|
||||
const isArmed = !!(node?.editor?._handdrawActive);
|
||||
// Armed when in create-new-stroke mode (separate from per-layer edit)
|
||||
const isArmed = (node?.editor?._handdrawMode === 'create');
|
||||
drawWidgetButton(
|
||||
ctx,
|
||||
{ size: [drawButtonWidth, height], pos: [posX, posY] },
|
||||
@@ -208,29 +215,58 @@ export class TopRowWidget extends RgthreeBaseWidget {
|
||||
);
|
||||
// Add blue outline when armed to match active layer style
|
||||
if (isArmed) {
|
||||
// Slightly inset outline to overlay nicely
|
||||
// Slightly inset outline to overlay nicely; match thickness with layer edit button (2px)
|
||||
const pad = 0.5;
|
||||
drawRoundedRectangle(ctx, {
|
||||
pos: [posX + pad, posY + pad],
|
||||
size: [drawButtonWidth - pad * 2, height - pad * 2],
|
||||
colorBackground: 'transparent',
|
||||
colorStroke: '#2cc6ff',
|
||||
borderRadius: 4
|
||||
});
|
||||
ctx.save();
|
||||
ctx.strokeStyle = '#2cc6ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(posX + pad, posY + pad, drawButtonWidth - pad * 2, height - pad * 2, [4]);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
this.hitAreas.drawButton = { bounds: [posX, drawButtonWidth], onClick: (e, p, n) => {
|
||||
// Toggle draw mode on the editor; canvas will handle capture on mousedown
|
||||
if (n?.editor) {
|
||||
if (n.editor._handdrawActive) {
|
||||
if (n.editor._handdrawMode === 'create') {
|
||||
n.editor.exitHanddrawMode?.(false);
|
||||
} else {
|
||||
n.editor.enterHanddrawMode?.();
|
||||
// enter create-new-stroke mode (each stroke creates a new handdraw layer)
|
||||
n.editor.enterHanddrawMode?.('create');
|
||||
}
|
||||
n.setDirtyCanvas(true, true);
|
||||
}
|
||||
return true;
|
||||
}};
|
||||
posX += drawButtonWidth + spacing;
|
||||
|
||||
// Animation toggle icon for inactive flow animation (default ON)
|
||||
const isAnimOn = !!(node?.editor?._inactiveFlowEnabled ?? true);
|
||||
drawWidgetButton(
|
||||
ctx,
|
||||
{ size: [drawButtonWidth, height], pos: [posX, posY] },
|
||||
"〰", // wavy line icon
|
||||
isAnimOn
|
||||
);
|
||||
if (isAnimOn) {
|
||||
const pad = 0.5;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = '#2cc6ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(posX + pad, posY + pad, drawButtonWidth - pad * 2, height - pad * 2, [4]);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
this.hitAreas.animToggleButton = { bounds: [posX, drawButtonWidth], onClick: (e, p, n) => {
|
||||
if (n?.editor) {
|
||||
n.editor._inactiveFlowEnabled = !n.editor._inactiveFlowEnabled;
|
||||
try { n.editor.layerRenderer?.updateInactiveDash?.(); } catch {}
|
||||
n.setDirtyCanvas(true, true);
|
||||
}
|
||||
return true;
|
||||
}};
|
||||
posX += drawButtonWidth + spacing;
|
||||
|
||||
// Draw rounded area for width/height controls
|
||||
const roundedAreaX = posX;
|
||||
|
||||
@@ -267,20 +267,27 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
|
||||
ctx.textAlign = "left";
|
||||
const nameText = fitString(ctx, this.value.name || "Spline", rposX - posX - innerMargin);
|
||||
ctx.fillText(nameText, posX, midY);
|
||||
this.hitAreas.name.bounds = [posX, 0, rposX - posX, height];
|
||||
// Make name hit area wide and simple (x + width only) to avoid y-mismatch
|
||||
this.hitAreas.name.bounds = [posX, rposX - posX];
|
||||
// Activate layer when clicking the name area (onDown for immediacy)
|
||||
this.hitAreas.name.onDown = (event, pos, node) => {
|
||||
if (!node.layerManager) return false;
|
||||
const current = node.layerManager.getActiveWidget?.();
|
||||
// Toggle off if clicking the currently active layer name
|
||||
if (current === this) {
|
||||
node.layerManager.setActiveWidget(null);
|
||||
} else {
|
||||
node.layerManager.setActiveWidget(this);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
this.hitAreas.name.onClick = (event, pos, node) => true; // handled onDown
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
onMouseDown(event, pos, node) {
|
||||
// Check if the click was on the toggle button
|
||||
if (this.hitAreas.toggle && this.clickWasWithinBounds(pos, this.hitAreas.toggle.bounds)) {
|
||||
// If it was on the toggle, do not set active widget.
|
||||
// The onToggleDown handler will take care of the toggle action.
|
||||
} else if (node.layerManager) {
|
||||
// Otherwise, set this widget as active
|
||||
node.layerManager.setActiveWidget(this);
|
||||
}
|
||||
// Activation is handled via hitAreas.name.onDown; do not duplicate here
|
||||
return super.onMouseDown?.(event, pos, node);
|
||||
}
|
||||
|
||||
@@ -508,11 +515,7 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
|
||||
onMouseUp(event, pos, node) {
|
||||
super.onMouseUp(event, pos, node);
|
||||
this.haveMouseMovedValue = false;
|
||||
|
||||
// Set this widget as active when clicked
|
||||
if (node.layerManager) {
|
||||
node.layerManager.setActiveWidget(this);
|
||||
}
|
||||
// Activation handled by hitAreas; no action here
|
||||
}
|
||||
|
||||
onDrivenToggleDown(event, pos, node) {
|
||||
|
||||
Reference in New Issue
Block a user