box layers added
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
# ComfyUI-WanVideoWrapper_QQ
|
||||
|
||||
## Updates
|
||||
**Version 1.0.7** - 2025-11-13
|
||||
- **Box layer type**: new layer type that can scale "points" type layer points when driving them (only points for now)
|
||||
- Right click drag in inner border of boxes to scale each one individually.
|
||||
- If updated spline editor messes up workflow , open it ,delete node , refresh page ,create new.
|
||||
|
||||
https://github.com/user-attachments/assets/e5849627-5f1a-4056-a941-a15b40bfc6c4
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**Version 1.0.6** - 2025-11-13
|
||||
- **New Wan22FMLF**: A better way to add midframe control from https://github.com/wallen0322/ComfyUI-Wan22FMLF + amplitude test in one node
|
||||
- **Lora high.low filter**: Clicking the circle icon cycles through: ◎ → ◐ → ◑ → ◎ + right icon right
|
||||
|
||||
+486
-222
File diff suppressed because it is too large
Load Diff
+123
-67
@@ -7,6 +7,8 @@ import math
|
||||
from torchvision import transforms
|
||||
from ..utility.driver_utils import apply_driver_offset
|
||||
|
||||
BOX_BASE_RADIUS = 56
|
||||
|
||||
class PowerSplineEditor:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -394,23 +396,36 @@ class PowerSplineEditor:
|
||||
all_coord_types: list = []
|
||||
all_coord_start_frames = list(incoming_coord_start_frames)
|
||||
all_coord_end_frames = list(incoming_coord_end_frames)
|
||||
all_box_paths = []
|
||||
all_box_names: list = []
|
||||
all_box_types: list = []
|
||||
all_box_start_frames = []
|
||||
all_box_end_frames = []
|
||||
|
||||
all_p_offsets = [] # Initialize list for p_coordinates offsets
|
||||
all_coord_offsets = [] # Initialize list for coordinates offsets
|
||||
all_box_offsets = [] # Initialize list for box offsets
|
||||
all_p_interpolations = [] # Initialize list for p_coordinates interpolations
|
||||
all_coord_interpolations = [] # Initialize list for coordinates interpolations
|
||||
all_box_interpolations = [] # Initialize list for box interpolations
|
||||
all_p_easing_functions = [] # Initialize list for p_coordinates easing functions
|
||||
all_coord_easing_functions = [] # Initialize list for coordinates easing functions
|
||||
all_box_easing_functions = [] # Initialize list for box easing functions
|
||||
all_p_easing_paths = [] # Initialize list for p_coordinates easing paths
|
||||
all_coord_easing_paths = [] # Initialize list for coordinates easing paths
|
||||
all_box_easing_paths = [] # Initialize list for box easing paths
|
||||
all_p_easing_strengths = [] # Initialize list for p_coordinates easing strengths
|
||||
all_coord_easing_strengths = [] # Initialize list for coordinates easing strengths
|
||||
all_box_easing_strengths = [] # Initialize list for box easing strengths
|
||||
all_p_accelerations = [] # Initialize list for p_coordinates accelerations
|
||||
all_coord_accelerations = [] # Initialize list for coordinates accelerations
|
||||
all_box_accelerations = [] # Initialize list for box accelerations
|
||||
all_p_scales = [] # Initialize list for p_coordinates scales
|
||||
all_coord_scales = [] # Initialize list for coordinates scales
|
||||
all_box_scales = [] # Initialize list for box scales
|
||||
all_p_drivers = [] # Driver info for p_coordinates
|
||||
all_coord_drivers = [] # Driver info for coordinates
|
||||
all_box_drivers = [] # Driver info for box coordinates
|
||||
|
||||
# Build layer lookup map for driver processing
|
||||
# Map layer names to their processed coordinates for driving
|
||||
@@ -546,6 +561,7 @@ class PowerSplineEditor:
|
||||
driver_acceleration = 0.00 # Default
|
||||
|
||||
# Search for the driver layer in all_splines to get its interpolation parameters
|
||||
driver_type = 'linear'
|
||||
for driver_spline_data in all_splines:
|
||||
if isinstance(driver_spline_data, dict) and driver_spline_data.get('name', '') == driver_name:
|
||||
driver_easing_function = driver_spline_data.get('easing', 'linear')
|
||||
@@ -553,6 +569,7 @@ class PowerSplineEditor:
|
||||
driver_easing_path = driver_easing_config.get('path', 'full')
|
||||
driver_easing_strength = driver_easing_config.get('strength', 1.0)
|
||||
driver_acceleration = driver_easing_config.get('acceleration', 0.00)
|
||||
driver_type = driver_spline_data.get('interpolation', 'linear')
|
||||
break
|
||||
|
||||
# Gather driver's own timing so consumers can respect it
|
||||
@@ -560,8 +577,37 @@ class PowerSplineEditor:
|
||||
driver_end_pause = int(driver_spline_data.get('z_pause', 0))
|
||||
driver_offset_val = int(driver_spline_data.get('offset', 0))
|
||||
|
||||
# Store driver info for ALL interpolation modes
|
||||
# Let draw_shapes.py apply the offset at the right time
|
||||
def extract_point_scale(point):
|
||||
if not isinstance(point, dict):
|
||||
return 1.0
|
||||
for key in ('boxScale', 'scale'):
|
||||
value = point.get(key)
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
return 1.0
|
||||
|
||||
driver_scale_profile = []
|
||||
driver_scale_factor = 1.0
|
||||
if driver_coords and isinstance(driver_coords, list) and len(driver_coords) > 0:
|
||||
base_scale = extract_point_scale(driver_coords[0]) or 1.0
|
||||
if abs(base_scale) < 1e-6:
|
||||
base_scale = 1.0
|
||||
for pt in driver_coords:
|
||||
scale_val = extract_point_scale(pt)
|
||||
ratio = scale_val / base_scale if base_scale else 1.0
|
||||
driver_scale_profile.append(ratio)
|
||||
driver_scale_factor = max(driver_scale_profile) if driver_scale_profile else 1.0
|
||||
driver_radius_delta = BOX_BASE_RADIUS * (driver_scale_factor - 1.0)
|
||||
driver_pivot = None
|
||||
if driver_coords and isinstance(driver_coords[0], dict):
|
||||
try:
|
||||
driver_pivot = (
|
||||
float(driver_coords[0].get('x', 0.0)),
|
||||
float(driver_coords[0].get('y', 0.0))
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
driver_pivot = None
|
||||
|
||||
driver_info_for_layer = {
|
||||
"path": driver_coords,
|
||||
"rotate": driver_rotate,
|
||||
@@ -573,7 +619,12 @@ class PowerSplineEditor:
|
||||
# Driver timing (used by DrawShapeOnPath to delay/advance driver motion)
|
||||
"start_pause": driver_start_pause,
|
||||
"end_pause": driver_end_pause,
|
||||
"offset": driver_offset_val
|
||||
"offset": driver_offset_val,
|
||||
"driver_type": driver_type,
|
||||
"driver_scale_profile": driver_scale_profile,
|
||||
"driver_scale_factor": driver_scale_factor,
|
||||
"driver_radius_delta": driver_radius_delta,
|
||||
"driver_pivot": driver_pivot
|
||||
}
|
||||
print(f"Stored driver '{driver_name}' for layer '{current_spline_name}' (mode={spline_interpolation}, rotate={driver_rotate}°, d_scale={driver_d_scale}, easing={driver_easing_function})")
|
||||
else:
|
||||
@@ -611,6 +662,20 @@ class PowerSplineEditor:
|
||||
all_p_accelerations.append(acceleration) # Collect acceleration value
|
||||
all_p_scales.append(scale) # Collect scale value
|
||||
all_p_drivers.append(driver_info_for_layer) # Collect driver info (None if no driver)
|
||||
elif spline_interpolation == 'box':
|
||||
all_box_paths.append(spline_coords)
|
||||
all_box_names.append(spline_data.get('name', ''))
|
||||
all_box_types.append('box')
|
||||
all_box_start_frames.append(start_frames)
|
||||
all_box_end_frames.append(end_frames)
|
||||
all_box_offsets.append(offset)
|
||||
all_box_interpolations.append(spline_interpolation)
|
||||
all_box_easing_functions.append(easing_function)
|
||||
all_box_easing_paths.append(easing_path)
|
||||
all_box_easing_strengths.append(easing_strength)
|
||||
all_box_accelerations.append(acceleration)
|
||||
all_box_scales.append(scale)
|
||||
all_box_drivers.append(driver_info_for_layer)
|
||||
else:
|
||||
all_coord_paths.append(spline_coords)
|
||||
all_coord_names.append(spline_data.get('name', ''))
|
||||
@@ -640,93 +705,84 @@ class PowerSplineEditor:
|
||||
coord_out_data = {}
|
||||
|
||||
# Add p_coordinates if present
|
||||
p_start_out = []
|
||||
p_end_out = []
|
||||
if all_p_paths:
|
||||
if len(all_p_paths) > 1:
|
||||
# Multiple paths - output as list of lists
|
||||
coord_out_data["p_coordinates"] = all_p_paths
|
||||
p_start_out = all_p_start_frames
|
||||
p_end_out = all_p_end_frames
|
||||
elif len(all_p_paths) == 1:
|
||||
# Single path - output as simple list for backward compatibility
|
||||
else:
|
||||
coord_out_data["p_coordinates"] = all_p_paths[0]
|
||||
p_start_out = all_p_start_frames[0] if all_p_start_frames else 0
|
||||
p_end_out = all_p_end_frames[0] if all_p_end_frames else 0
|
||||
else:
|
||||
p_start_out = []
|
||||
p_end_out = []
|
||||
|
||||
# Add coordinates if present
|
||||
c_start_out = []
|
||||
c_end_out = []
|
||||
if all_coord_paths:
|
||||
if len(all_coord_paths) > 1:
|
||||
# Multiple paths - output as list of lists
|
||||
coord_out_data["coordinates"] = all_coord_paths
|
||||
c_start_out = all_coord_start_frames
|
||||
c_end_out = all_coord_end_frames
|
||||
elif len(all_coord_paths) == 1:
|
||||
# Single path - output as simple list for backward compatibility
|
||||
else:
|
||||
coord_out_data["coordinates"] = all_coord_paths[0]
|
||||
c_start_out = all_coord_start_frames[0] if all_coord_start_frames else 0
|
||||
c_end_out = all_coord_end_frames[0] if all_coord_end_frames else 0
|
||||
else:
|
||||
c_start_out = []
|
||||
c_end_out = []
|
||||
|
||||
# Add pause frames, offsets, interpolations, easing params, scales, and drivers based on what exists
|
||||
if all_p_paths and all_coord_paths:
|
||||
# Both types exist - use new format with separate pause frames, offsets, interpolations, easing params, scales, and drivers
|
||||
coord_out_data["start_p_frames"] = {"p": p_start_out, "c": c_start_out}
|
||||
coord_out_data["end_p_frames"] = {"p": p_end_out, "c": c_end_out}
|
||||
coord_out_data["offsets"] = {"p": all_p_offsets, "c": all_coord_offsets}
|
||||
coord_out_data["interpolations"] = {"p": all_p_interpolations, "c": all_coord_interpolations}
|
||||
coord_out_data["easing_functions"] = {"p": all_p_easing_functions, "c": all_coord_easing_functions}
|
||||
coord_out_data["easing_paths"] = {"p": all_p_easing_paths, "c": all_coord_easing_paths}
|
||||
coord_out_data["easing_strengths"] = {"p": all_p_easing_strengths, "c": all_coord_easing_strengths}
|
||||
coord_out_data["accelerations"] = {"p": all_p_accelerations, "c": all_coord_accelerations}
|
||||
coord_out_data["scales"] = {"p": all_p_scales, "c": all_coord_scales}
|
||||
coord_out_data["drivers"] = {"p": all_p_drivers, "c": all_coord_drivers}
|
||||
coord_out_data["names"] = {"p": all_p_names, "c": all_coord_names}
|
||||
coord_out_data["types"] = {"p": all_p_types, "c": all_coord_types}
|
||||
elif all_p_paths:
|
||||
# Only p_coordinates - use new format for consistency
|
||||
coord_out_data["start_p_frames"] = {"p": p_start_out, "c": []}
|
||||
coord_out_data["end_p_frames"] = {"p": p_end_out, "c": []}
|
||||
coord_out_data["offsets"] = {"p": all_p_offsets, "c": []}
|
||||
coord_out_data["interpolations"] = {"p": all_p_interpolations, "c": []}
|
||||
coord_out_data["easing_functions"] = {"p": all_p_easing_functions, "c": []}
|
||||
coord_out_data["easing_paths"] = {"p": all_p_easing_paths, "c": []}
|
||||
coord_out_data["easing_strengths"] = {"p": all_p_easing_strengths, "c": []}
|
||||
coord_out_data["accelerations"] = {"p": all_p_accelerations, "c": []}
|
||||
coord_out_data["scales"] = {"p": all_p_scales, "c": []}
|
||||
coord_out_data["drivers"] = {"p": all_p_drivers, "c": []}
|
||||
coord_out_data["names"] = {"p": all_p_names, "c": []}
|
||||
coord_out_data["types"] = {"p": all_p_types, "c": []}
|
||||
elif all_coord_paths:
|
||||
# Only coordinates - use new format for consistency
|
||||
coord_out_data["start_p_frames"] = {"p": [], "c": c_start_out}
|
||||
coord_out_data["end_p_frames"] = {"p": [], "c": c_end_out}
|
||||
coord_out_data["offsets"] = {"p": [], "c": all_coord_offsets}
|
||||
coord_out_data["interpolations"] = {"p": [], "c": all_coord_interpolations}
|
||||
coord_out_data["easing_functions"] = {"p": [], "c": all_coord_easing_functions}
|
||||
coord_out_data["easing_paths"] = {"p": [], "c": all_coord_easing_paths}
|
||||
coord_out_data["easing_strengths"] = {"p": [], "c": all_coord_easing_strengths}
|
||||
coord_out_data["accelerations"] = {"p": [], "c": all_coord_accelerations}
|
||||
coord_out_data["scales"] = {"p": [], "c": all_coord_scales}
|
||||
coord_out_data["drivers"] = {"p": [], "c": all_coord_drivers}
|
||||
coord_out_data["names"] = {"p": [], "c": all_coord_names}
|
||||
coord_out_data["types"] = {"p": [], "c": all_coord_types}
|
||||
else:
|
||||
# Add box coordinates if present
|
||||
b_start_out = []
|
||||
b_end_out = []
|
||||
if all_box_paths:
|
||||
if len(all_box_paths) > 1:
|
||||
coord_out_data["box_coordinates"] = all_box_paths
|
||||
b_start_out = all_box_start_frames
|
||||
b_end_out = all_box_end_frames
|
||||
else:
|
||||
coord_out_data["box_coordinates"] = all_box_paths[0]
|
||||
b_start_out = all_box_start_frames[0] if all_box_start_frames else 0
|
||||
b_end_out = all_box_end_frames[0] if all_box_end_frames else 0
|
||||
|
||||
def assemble_meta(p_has, p_val, c_has, c_val, b_has, b_val):
|
||||
if not (p_has or c_has or b_has):
|
||||
return []
|
||||
return {
|
||||
"p": p_val if p_has else [],
|
||||
"c": c_val if c_has else [],
|
||||
"b": b_val if b_has else []
|
||||
}
|
||||
|
||||
has_p = bool(all_p_paths)
|
||||
has_c = bool(all_coord_paths)
|
||||
has_b = bool(all_box_paths)
|
||||
if not (has_p or has_c or has_b):
|
||||
# No paths at all
|
||||
coord_out_data["start_p_frames"] = 0
|
||||
coord_out_data["end_p_frames"] = 0
|
||||
coord_out_data["offsets"] = [] # Default empty list
|
||||
coord_out_data["interpolations"] = [] # Default empty list
|
||||
coord_out_data["easing_functions"] = [] # Default empty list for easing functions
|
||||
coord_out_data["easing_paths"] = [] # Default empty list for easing paths
|
||||
coord_out_data["easing_strengths"] = [] # Default empty list for easing strengths
|
||||
coord_out_data["accelerations"] = [] # Default empty list for accelerations
|
||||
coord_out_data["scales"] = [] # Default empty list for scales
|
||||
coord_out_data["drivers"] = [] # Default empty list
|
||||
coord_out_data["offsets"] = [] # Default empty list
|
||||
coord_out_data["interpolations"] = [] # Default empty list
|
||||
coord_out_data["easing_functions"] = [] # Default empty list for easing functions
|
||||
coord_out_data["easing_paths"] = [] # Default empty list for easing paths
|
||||
coord_out_data["easing_strengths"] = [] # Default empty list for easing strengths
|
||||
coord_out_data["accelerations"] = [] # Default empty list for accelerations
|
||||
coord_out_data["scales"] = [] # Default empty list for scales
|
||||
coord_out_data["drivers"] = [] # Default empty list
|
||||
coord_out_data["names"] = {"p": [], "c": [], "b": []}
|
||||
coord_out_data["types"] = {"p": [], "c": [], "b": []}
|
||||
print("Warning: No paths to output")
|
||||
else:
|
||||
coord_out_data["start_p_frames"] = assemble_meta(has_p, p_start_out, has_c, c_start_out, has_b, b_start_out)
|
||||
coord_out_data["end_p_frames"] = assemble_meta(has_p, p_end_out, has_c, c_end_out, has_b, b_end_out)
|
||||
coord_out_data["offsets"] = assemble_meta(has_p, all_p_offsets, has_c, all_coord_offsets, has_b, all_box_offsets)
|
||||
coord_out_data["interpolations"] = assemble_meta(has_p, all_p_interpolations, has_c, all_coord_interpolations, has_b, all_box_interpolations)
|
||||
coord_out_data["easing_functions"] = assemble_meta(has_p, all_p_easing_functions, has_c, all_coord_easing_functions, has_b, all_box_easing_functions)
|
||||
coord_out_data["easing_paths"] = assemble_meta(has_p, all_p_easing_paths, has_c, all_coord_easing_paths, has_b, all_box_easing_paths)
|
||||
coord_out_data["easing_strengths"] = assemble_meta(has_p, all_p_easing_strengths, has_c, all_coord_easing_strengths, has_b, all_box_easing_strengths)
|
||||
coord_out_data["accelerations"] = assemble_meta(has_p, all_p_accelerations, has_c, all_coord_accelerations, has_b, all_box_accelerations)
|
||||
coord_out_data["scales"] = assemble_meta(has_p, all_p_scales, has_c, all_coord_scales, has_b, all_box_scales)
|
||||
coord_out_data["drivers"] = assemble_meta(has_p, all_p_drivers, has_c, all_coord_drivers, has_b, all_box_drivers)
|
||||
coord_out_data["names"] = assemble_meta(has_p, all_p_names, has_c, all_coord_names, has_b, all_box_names)
|
||||
coord_out_data["types"] = assemble_meta(has_p, all_p_types, has_c, all_coord_types, has_b, all_box_types)
|
||||
|
||||
# Include coordinate space dimensions so DrawShapeOnPath can scale if needed
|
||||
# Coordinates from the frontend are in normalized 0-1 range
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "wanvideowrapper_qq"
|
||||
description = "A collection of custom nodes for ComfyUI providing video manipulation, LoRA loading, path animation, and visual drawing tools for WanVideo workflows."
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
license = {file = "LICENSE"}
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
|
||||
@@ -496,7 +496,7 @@ export function showInterpolationMenu(event, widget, position) {
|
||||
transform: translateX(-40%) !important;
|
||||
`;
|
||||
|
||||
const options = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const options = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const current = widget.value.interpolation || 'linear';
|
||||
|
||||
const list = document.createElement('div');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BOX_BASE_RADIUS, POINT_BASE_RADIUS } from './spline_utils.js';
|
||||
|
||||
/**
|
||||
* @class LayerRenderer
|
||||
* A dedicated class to handle the rendering of active and inactive spline layers
|
||||
@@ -28,6 +30,27 @@ export class LayerRenderer {
|
||||
}, 80);
|
||||
}
|
||||
|
||||
clampPointScale(value) {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(0.2, Math.min(3.0, value));
|
||||
}
|
||||
|
||||
getPointScale(point, forBox = false) {
|
||||
if (!point) return 1;
|
||||
if (forBox && typeof point.boxScale === 'number' && !Number.isNaN(point.boxScale)) {
|
||||
return this.clampPointScale(point.boxScale);
|
||||
}
|
||||
if (!forBox && typeof point.pointScale === 'number' && !Number.isNaN(point.pointScale)) {
|
||||
return this.clampPointScale(point.pointScale);
|
||||
}
|
||||
if (typeof point.scale === 'number' && !Number.isNaN(point.scale)) {
|
||||
return this.clampPointScale(point.scale);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Apply/update marching-dash on inactive paths via DOM attributes
|
||||
updateInactiveDash() {
|
||||
if (!this.splineEditor) return;
|
||||
@@ -100,7 +123,10 @@ export class LayerRenderer {
|
||||
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 orangeMetas = (this.inactiveLayerMetadata || []).filter(d => {
|
||||
const interp = d?.widget?.value?.interpolation || 'linear';
|
||||
return (d?.widget?.value?.type !== 'handdraw') && (interp !== 'points' && interp !== 'box');
|
||||
});
|
||||
const applyFor = (pairs, metas) => {
|
||||
const count = Math.min(pairs.length, metas.length);
|
||||
for (let idx = 0; idx < count; idx++) {
|
||||
@@ -289,10 +315,23 @@ export class LayerRenderer {
|
||||
* @param {object} widget - The active spline widget.
|
||||
*/
|
||||
drawActiveLayer(widget) {
|
||||
const points = this.splineEditor.points;
|
||||
if (!points || points.length === 0) return;
|
||||
const points = Array.isArray(this.splineEditor.points) ? this.splineEditor.points : [];
|
||||
if (!points.length) return;
|
||||
const pointIndexMap = new Map(points.map((p, i) => [p, i]));
|
||||
|
||||
const interpolation = widget.value.interpolation || 'linear';
|
||||
const isPointMode = (interpolation === 'points' || interpolation === 'box');
|
||||
const isBoxPointMode = interpolation === 'box';
|
||||
const pointMarkerShape = isBoxPointMode ? 'square' : 'circle';
|
||||
const orderedDots = isPointMode
|
||||
? [...points].sort((a, b) => this.splineEditor.getBoxPointRadius(b) - this.splineEditor.getBoxPointRadius(a))
|
||||
: points;
|
||||
const getPointIndex = (dot) => {
|
||||
if (!dot) return -1;
|
||||
if (pointIndexMap.has(dot)) return pointIndexMap.get(dot);
|
||||
if (this.splineEditor.resolvePointIndex) return this.splineEditor.resolvePointIndex(dot);
|
||||
return this.splineEditor.points.indexOf(dot);
|
||||
};
|
||||
// Treat preview drawing as handdraw for styling when armed
|
||||
const isHanddraw = (widget?.value?.type === 'handdraw') || !!this.splineEditor._handdrawActive;
|
||||
|
||||
@@ -302,7 +341,7 @@ export class LayerRenderer {
|
||||
this.activeLayerPanel.add(pv.Line)
|
||||
.data(() => {
|
||||
// Special handling for curve rendering when points are highlighted
|
||||
if (interpolation !== 'points' && interpolation !== 'linear') {
|
||||
if (!isPointMode && interpolation !== 'linear') {
|
||||
const hasAnyHighlighted = points.some(p => p.highlighted);
|
||||
if (hasAnyHighlighted) {
|
||||
// Insert duplicate points at highlighted positions to force linear segments
|
||||
@@ -350,7 +389,7 @@ export class LayerRenderer {
|
||||
})
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.interpolate(() => interpolation === 'points' ? 'linear' : interpolation)
|
||||
.interpolate(() => isPointMode ? 'linear' : interpolation)
|
||||
.strokeStyle(() => {
|
||||
if (isHanddraw) {
|
||||
return '#d7c400'; // muted yellow for active handdraw
|
||||
@@ -360,21 +399,28 @@ export class LayerRenderer {
|
||||
.lineWidth(() => {
|
||||
// Handdraw (active) should have same thickness as normal lines
|
||||
if (isHanddraw) return 3;
|
||||
return interpolation === 'points' ? 1 : 3;
|
||||
return isPointMode ? 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)
|
||||
.data(() => orderedDots)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.radius(() => {
|
||||
if (isHanddraw) return 2; // make yellow hand-draw points smaller
|
||||
return interpolation === 'points' ? 9 : 6;
|
||||
.radius((dot) => {
|
||||
if (isHanddraw) return 2;
|
||||
if (interpolation === 'box') {
|
||||
return BOX_BASE_RADIUS * this.getPointScale(dot, true);
|
||||
}
|
||||
if (interpolation === 'points') {
|
||||
return POINT_BASE_RADIUS;
|
||||
}
|
||||
return 6;
|
||||
})
|
||||
.shape((dot) => {
|
||||
if (interpolation === 'points') return "circle";
|
||||
if (interpolation === 'box') return "square";
|
||||
// If interpolation is 'basis' AND the point is highlighted, draw a square
|
||||
if (interpolation === 'basis' && dot.highlighted) return "square";
|
||||
const index = this.splineEditor.resolvePointIndex
|
||||
@@ -382,7 +428,7 @@ export class LayerRenderer {
|
||||
: this.splineEditor.points.indexOf(dot);
|
||||
return index === 0 ? "triangle" : "circle";
|
||||
}) .angle((dot) => {
|
||||
if (interpolation === 'points') return 0;
|
||||
if (isPointMode) return 0;
|
||||
const index = this.splineEditor.resolvePointIndex
|
||||
? this.splineEditor.resolvePointIndex(dot)
|
||||
: this.splineEditor.points.indexOf(dot);
|
||||
@@ -396,26 +442,26 @@ export class LayerRenderer {
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.cursor("move")
|
||||
.cursor(() => (isPointMode ? "default" : "move"))
|
||||
.strokeStyle((dot) => {
|
||||
const index = this.splineEditor.resolvePointIndex
|
||||
? this.splineEditor.resolvePointIndex(dot)
|
||||
: this.splineEditor.points.indexOf(dot);
|
||||
if (!isHanddraw && index === 0 && interpolation !== 'points') {
|
||||
if (!isHanddraw && index === 0 && !isPointMode) {
|
||||
return "green";
|
||||
}
|
||||
if (isHanddraw) return '#d7c400';
|
||||
return interpolation === 'points' ? "#139613" : "#1f77b4";
|
||||
return isPointMode ? "#139613" : "#1f77b4";
|
||||
})
|
||||
.fillStyle((dot) => {
|
||||
const index = this.splineEditor.resolvePointIndex
|
||||
? this.splineEditor.resolvePointIndex(dot)
|
||||
: this.splineEditor.points.indexOf(dot);
|
||||
if (!isHanddraw && index === 0 && interpolation !== 'points') {
|
||||
if (!isHanddraw && index === 0 && !isPointMode) {
|
||||
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)";
|
||||
return isPointMode ? "rgba(19, 150, 19, 0.1)" : "rgba(100, 100, 100, 0.5)";
|
||||
})
|
||||
.event("mousedown", (dot) => {
|
||||
if (this.splineEditor.handlePointPointerDown) {
|
||||
@@ -424,6 +470,22 @@ export class LayerRenderer {
|
||||
})
|
||||
.event("mouseover", this.splineEditor.mouseOverHandler)
|
||||
.event("mouseout", this.splineEditor.mouseOutHandler);
|
||||
|
||||
if (interpolation === 'box') {
|
||||
this.activeLayerPanel.add(pv.Label)
|
||||
.data(() => orderedDots)
|
||||
.left(d => d.x - (BOX_BASE_RADIUS * this.getPointScale(d, true)) + 6)
|
||||
.top(d => d.y - (BOX_BASE_RADIUS * this.getPointScale(d, true)) + 7)
|
||||
.text((dot) => {
|
||||
const idx = getPointIndex(dot);
|
||||
return idx >= 0 ? `${idx + 1}` : "";
|
||||
})
|
||||
.textAlign("left")
|
||||
.textBaseline("top")
|
||||
.font("14px monospace")
|
||||
.textStyle("#4dc55a")
|
||||
.events("none");
|
||||
}
|
||||
// No labels for any mode - removed the label anchor
|
||||
}
|
||||
|
||||
@@ -465,8 +527,11 @@ export class LayerRenderer {
|
||||
});
|
||||
|
||||
const interpolation = widget.value.interpolation || 'linear';
|
||||
const isPointMode = (interpolation === 'points' || interpolation === 'box');
|
||||
const isBoxPointMode = interpolation === 'box';
|
||||
const pointMarkerShape = isBoxPointMode ? 'square' : 'circle';
|
||||
// For inactive points layers (non-handdraw), reduce line opacity to 20%
|
||||
if (!isHanddraw && !isOff && interpolation === 'points') {
|
||||
if (!isHanddraw && !isOff && isPointMode) {
|
||||
strokeColor = "rgba(255, 127, 14, 0.2)";
|
||||
}
|
||||
|
||||
@@ -475,7 +540,7 @@ export class LayerRenderer {
|
||||
|
||||
// For inactive layers, we also need to handle highlighted points for consistency
|
||||
let renderPoints = pointsSnapshot;
|
||||
if (interpolation !== 'points' && interpolation !== 'linear') {
|
||||
if (!isPointMode && interpolation !== 'linear') {
|
||||
const hasAnyHighlighted = points.some(p => p.highlighted);
|
||||
if (hasAnyHighlighted) {
|
||||
// Insert duplicate points at highlighted positions to force linear segments
|
||||
@@ -510,12 +575,12 @@ export class LayerRenderer {
|
||||
}
|
||||
|
||||
// ALWAYS draw line
|
||||
const lineWidthMain = (interpolation === 'points') ? (isHanddraw ? 3 : 1) : 3;
|
||||
const lineWidthMain = isPointMode ? (isHanddraw ? 3 : 1) : 3;
|
||||
const inactiveLine = layerPanel.add(pv.Line).events("none")
|
||||
.data(renderPoints)
|
||||
.left(d => d.x)
|
||||
.top(d => d.y)
|
||||
.interpolate(interpolation === 'points' ? 'linear' : interpolation)
|
||||
.interpolate(isPointMode ? 'linear' : interpolation)
|
||||
.strokeStyle(strokeColor)
|
||||
.lineWidth(lineWidthMain);
|
||||
// Note: dash animation is applied post-render via updateInactiveDash()
|
||||
@@ -526,7 +591,7 @@ export class LayerRenderer {
|
||||
}
|
||||
|
||||
// Handle different interpolation modes for inactive layers
|
||||
if (interpolation === 'points') {
|
||||
if (isPointMode) {
|
||||
const lineWidthPts = isHanddraw ? 3 : 1;
|
||||
layerPanel.add(pv.Line).events("none")
|
||||
.data(pointsSnapshot)
|
||||
@@ -537,62 +602,25 @@ export class LayerRenderer {
|
||||
.lineWidth(lineWidthPts);
|
||||
// For points-type layers, also show point markers when inactive (non-handdraw only)
|
||||
if (!isHanddraw && pointsSnapshot.length > 0) {
|
||||
const inactiveDots = isPointMode
|
||||
? [...pointsSnapshot].sort((a, b) => this.splineEditor.getBoxPointRadius(b) - this.splineEditor.getBoxPointRadius(a))
|
||||
: pointsSnapshot;
|
||||
layerPanel.add(pv.Dot).events("none")
|
||||
.data(pointsSnapshot)
|
||||
.data(inactiveDots)
|
||||
.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)
|
||||
.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;
|
||||
});
|
||||
.radius((dot) => {
|
||||
if (isBoxPointMode) {
|
||||
return BOX_BASE_RADIUS * this.getPointScale(dot, true);
|
||||
}
|
||||
return POINT_BASE_RADIUS;
|
||||
})
|
||||
.shape(pointMarkerShape)
|
||||
.strokeStyle(strokeColor)
|
||||
.fillStyle(isPointMode ? "rgba(19, 150, 19, 0.1)" : fillColor);
|
||||
}
|
||||
} else {
|
||||
// ONLY draw triangle at first point for all other modes
|
||||
if (!isHanddraw && 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;
|
||||
});
|
||||
}
|
||||
// No triangles for other modes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { LayerRenderer } from './layer_renderer.js';
|
||||
import { BOX_BASE_RADIUS, BOX_BORDER_BAND } from './spline_utils.js';
|
||||
|
||||
export default class SplineEditor2 {
|
||||
constructor(context, reset = false) {
|
||||
@@ -129,7 +130,13 @@ export default class SplineEditor2 {
|
||||
try {
|
||||
const points = JSON.parse(activeWidget.value.points_store || '[]');
|
||||
const denorm = this.denormalizePoints(points);
|
||||
return denorm;
|
||||
this.ensurePointScaleFields(denorm);
|
||||
this.ensurePointUids(denorm);
|
||||
return denorm.map(point => ({
|
||||
...point,
|
||||
scale: (typeof point.scale === 'number' ? point.scale : 1.0),
|
||||
uid: point.uid
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("[SplineEditor] getActivePoints: failed to parse points_store", e);
|
||||
return [];
|
||||
@@ -142,6 +149,7 @@ export default class SplineEditor2 {
|
||||
console.warn("[SplineEditor] setActivePoints: no active widget");
|
||||
return;
|
||||
}
|
||||
this.ensurePointUids(points);
|
||||
activeWidget.value.points_store = JSON.stringify(this.normalizePoints(points));
|
||||
// Only call updatePath if vis is already created
|
||||
if (this.vis) {
|
||||
@@ -260,12 +268,79 @@ export default class SplineEditor2 {
|
||||
this.vis.render();
|
||||
}
|
||||
|
||||
this.nextPointUid = 1;
|
||||
this.ensurePointUids = (points) => {
|
||||
if (!Array.isArray(points)) return;
|
||||
let nextUid = this.nextPointUid || 1;
|
||||
for (const point of points) {
|
||||
if (!point) continue;
|
||||
const rawUid = Number(point.uid);
|
||||
const hasUid = (point.uid !== undefined && point.uid !== null && !Number.isNaN(rawUid));
|
||||
if (hasUid) {
|
||||
const candidate = Math.max(nextUid, rawUid + 1);
|
||||
nextUid = candidate;
|
||||
} else {
|
||||
point.uid = nextUid;
|
||||
nextUid += 1;
|
||||
}
|
||||
}
|
||||
this.nextPointUid = nextUid;
|
||||
};
|
||||
|
||||
this.clampScaleValue = (value) => {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return 1;
|
||||
return Math.max(0.2, Math.min(3.0, value));
|
||||
};
|
||||
|
||||
this.ensurePointScaleFields = (points) => {
|
||||
if (!Array.isArray(points)) return;
|
||||
for (const point of points) {
|
||||
if (!point) continue;
|
||||
const boxRaw = (typeof point.boxScale === 'number' && !Number.isNaN(point.boxScale))
|
||||
? point.boxScale
|
||||
: ((typeof point.scale === 'number' && !Number.isNaN(point.scale)) ? point.scale : 1);
|
||||
point.boxScale = this.clampScaleValue(boxRaw);
|
||||
|
||||
const pointRaw = (typeof point.pointScale === 'number' && !Number.isNaN(point.pointScale))
|
||||
? point.pointScale
|
||||
: 1;
|
||||
point.pointScale = this.clampScaleValue(pointRaw);
|
||||
|
||||
if (typeof point.scale !== 'number' || Number.isNaN(point.scale)) {
|
||||
point.scale = point.boxScale;
|
||||
} else {
|
||||
point.scale = this.clampScaleValue(point.scale);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.getPointScaleForMode = (point, forBox = true) => {
|
||||
if (!point) return 1;
|
||||
const candidate = (() => {
|
||||
if (forBox && typeof point.boxScale === 'number' && !Number.isNaN(point.boxScale)) {
|
||||
return point.boxScale;
|
||||
}
|
||||
if (!forBox && typeof point.pointScale === 'number' && !Number.isNaN(point.pointScale)) {
|
||||
return point.pointScale;
|
||||
}
|
||||
if (typeof point.scale === 'number' && !Number.isNaN(point.scale)) {
|
||||
return point.scale;
|
||||
}
|
||||
return 1;
|
||||
})();
|
||||
return this.clampScaleValue(candidate);
|
||||
};
|
||||
|
||||
// Initialize or reset points array
|
||||
this.drawRuler = true;
|
||||
this.hoverIndex = -1;
|
||||
this.isDragging = false;
|
||||
this.isDraggingAll = false; // Flag for Alt+middle-drag (translation)
|
||||
this.isScalingAll = false; // Flag for Alt+right-drag (scaling)
|
||||
this.isScalingPoint = false; // Flag for Alt+right-drag when box mode (point scale)
|
||||
this.scalingPointIndex = -1;
|
||||
this.scalingPointBaseScale = 1.0;
|
||||
this.scalingPointInitialDistance = 0;
|
||||
this.isRotatingAll = false; // Flag for Alt+left-drag (rotation)
|
||||
this.dragStartPos = null; // Store initial drag position
|
||||
this.initialXDistance = 0; // Store initial X distance for horizontal scaling
|
||||
@@ -290,16 +365,18 @@ export default class SplineEditor2 {
|
||||
// Two points: center and 40px right
|
||||
const centerX = this.width / 2;
|
||||
const centerY = this.height / 2;
|
||||
this.points = [
|
||||
{ x: centerX, y: centerY, highlighted: false },
|
||||
{ x: centerX + 40, y: centerY, highlighted: false }
|
||||
this.points = [
|
||||
{ x: centerX, y: centerY, highlighted: false, scale: 1.0, boxScale: 1.0, pointScale: 1.0 },
|
||||
{ x: centerX + 40, y: centerY, highlighted: false, scale: 1.0, boxScale: 1.0, pointScale: 1.0 }
|
||||
];
|
||||
this.ensurePointUids(this.points);
|
||||
} else {
|
||||
// Default initial points (bottom-left, top-right)
|
||||
this.points = [
|
||||
{ x: 0, y: this.height, highlighted: false },
|
||||
{ x: this.width, y: 0, highlighted: false }
|
||||
{ x: 0, y: this.height, highlighted: false, scale: 1.0, boxScale: 1.0, pointScale: 1.0 },
|
||||
{ x: this.width, y: 0, highlighted: false, scale: 1.0, boxScale: 1.0, pointScale: 1.0 }
|
||||
];
|
||||
this.ensurePointUids(this.points);
|
||||
}
|
||||
this.setActivePoints(this.points);
|
||||
}
|
||||
@@ -415,7 +492,11 @@ export default class SplineEditor2 {
|
||||
let scaledMouse = {
|
||||
x: mouseX,
|
||||
y: mouseY,
|
||||
highlighted: false
|
||||
highlighted: false,
|
||||
scale: 1.0,
|
||||
boxScale: 1.0,
|
||||
pointScale: 1.0,
|
||||
uid: this.nextPointUid++
|
||||
};
|
||||
this.i = this.points.push(scaledMouse) - 1;
|
||||
this.updatePath();
|
||||
@@ -435,7 +516,11 @@ export default class SplineEditor2 {
|
||||
let midpoint = {
|
||||
x: (this.points[point1Index].x + this.points[point2Index].x) / 2,
|
||||
y: (this.points[point1Index].y + this.points[point2Index].y) / 2,
|
||||
highlighted: false
|
||||
highlighted: false,
|
||||
scale: 1.0,
|
||||
boxScale: 1.0,
|
||||
pointScale: 1.0,
|
||||
uid: this.nextPointUid++
|
||||
};
|
||||
|
||||
// Insert the midpoint into the array
|
||||
@@ -502,11 +587,61 @@ export default class SplineEditor2 {
|
||||
return this;
|
||||
}
|
||||
})
|
||||
|
||||
.event("mousemove", (e) => {
|
||||
const coords = this._getPointerCoords(pv.event || e);
|
||||
this.updateBoxCursor(coords);
|
||||
})
|
||||
.event("mouseout", () => {
|
||||
const canvasEl = this.vis?.canvas?.();
|
||||
if (canvasEl) canvasEl.style.cursor = 'default';
|
||||
})
|
||||
|
||||
this.getBoxPointRadius = (point) => {
|
||||
const boxScale = this.getPointScaleForMode(point, true);
|
||||
return BOX_BASE_RADIUS * boxScale;
|
||||
};
|
||||
|
||||
this.pickBoxPointFromCoords = (coords) => {
|
||||
if (!coords || !Array.isArray(this.points)) return null;
|
||||
const bandWidth = BOX_BORDER_BAND;
|
||||
let best = null;
|
||||
let bestEdgeDist = Infinity;
|
||||
for (let idx = 0; idx < this.points.length; idx++) {
|
||||
const point = this.points[idx];
|
||||
if (!point) continue;
|
||||
const dx = Math.abs(coords.x - (point.x ?? 0));
|
||||
const dy = Math.abs(coords.y - (point.y ?? 0));
|
||||
const maxDist = Math.max(dx, dy); // square border distance
|
||||
const radius = this.getBoxPointRadius(point);
|
||||
const inner = Math.max(radius - bandWidth, 0);
|
||||
if (maxDist < inner || maxDist > radius) {
|
||||
continue;
|
||||
}
|
||||
const edgeDist = Math.abs(radius - maxDist);
|
||||
if (
|
||||
edgeDist < bestEdgeDist ||
|
||||
(Math.abs(edgeDist - bestEdgeDist) < 1e-3 && radius < (best?.radius ?? Infinity))
|
||||
) {
|
||||
bestEdgeDist = edgeDist;
|
||||
best = { point, index: idx, radius };
|
||||
}
|
||||
}
|
||||
return best;
|
||||
};
|
||||
|
||||
this.resolvePointIndex = (dot) => {
|
||||
if (!Array.isArray(this.points) || !dot) {
|
||||
return -1;
|
||||
}
|
||||
const dotUid = dot?.uid;
|
||||
if (dotUid !== undefined && dotUid !== null) {
|
||||
for (let idx = 0; idx < this.points.length; idx++) {
|
||||
const p = this.points[idx];
|
||||
if (p && (p.uid === dotUid)) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
const directIdx = this.points.indexOf(dot);
|
||||
if (directIdx !== -1) {
|
||||
return directIdx;
|
||||
@@ -546,20 +681,46 @@ export default class SplineEditor2 {
|
||||
};
|
||||
};
|
||||
|
||||
this.updateBoxCursor = (coords) => {
|
||||
const canvasEl = this.vis?.canvas?.();
|
||||
if (!canvasEl) return;
|
||||
const activeInterp = this.getActiveWidget()?.value?.interpolation || this.interpolation;
|
||||
let desired = 'default';
|
||||
if (activeInterp === 'box' && coords) {
|
||||
const picked = this.pickBoxPointFromCoords(coords);
|
||||
if (picked) {
|
||||
desired = 'move';
|
||||
}
|
||||
}
|
||||
if (canvasEl.style.cursor !== desired) {
|
||||
canvasEl.style.cursor = desired;
|
||||
}
|
||||
};
|
||||
|
||||
this.handlePointPointerDown = (dot, pvEvent) => {
|
||||
const event = pvEvent || window.event;
|
||||
if (!event) return;
|
||||
const { x, y } = this._getPointerCoords(event);
|
||||
const activeInterp = this.getActiveWidget()?.value?.interpolation || this.interpolation;
|
||||
let dragDot = dot;
|
||||
if (activeInterp === 'box') {
|
||||
const picked = this.pickBoxPointFromCoords({ x, y });
|
||||
if (!picked) {
|
||||
return;
|
||||
}
|
||||
dragDot = picked.point;
|
||||
}
|
||||
event.preventDefault?.();
|
||||
event.stopPropagation?.();
|
||||
const { x, y } = this._getPointerCoords(event);
|
||||
this.dragStartHandler(dot, x, y, event);
|
||||
this.dragStartHandler(dragDot, x, y, event);
|
||||
const dragTarget = dragDot;
|
||||
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);
|
||||
this.dragHandler(dragTarget, coords.x, coords.y);
|
||||
};
|
||||
const end = (evt) => {
|
||||
cleanup();
|
||||
@@ -647,6 +808,23 @@ export default class SplineEditor2 {
|
||||
|
||||
// Alt+Right-Click: Scaling relative to clicked point (any point)
|
||||
if ((rawEvent?.altKey) && (rawEvent?.button === 2)) {
|
||||
const activeInterp = this.getActiveWidget()?.value?.interpolation || this.interpolation;
|
||||
if (activeInterp === 'box') {
|
||||
this.isScalingPoint = true;
|
||||
this.scalingPointIndex = this.i;
|
||||
const targetPoint = this.points[this.i];
|
||||
if (!targetPoint) {
|
||||
this.isScalingPoint = false;
|
||||
return;
|
||||
}
|
||||
this.scalingPointBaseScale = (targetPoint.scale || 1.0);
|
||||
this.scalingPointInitialDistance = localX - targetPoint.x;
|
||||
if (Math.abs(this.scalingPointInitialDistance) < 10) {
|
||||
this.scalingPointInitialDistance = this.scalingPointInitialDistance >= 0 ? 10 : -10;
|
||||
}
|
||||
this.isDragging = true;
|
||||
return;
|
||||
}
|
||||
this.isScalingAll = true;
|
||||
this.originalPoints = this.points.map(p => ({ ...p }));
|
||||
this.anchorPoint = { ...this.points[this.i] }; // Use clicked point as anchor
|
||||
@@ -682,6 +860,10 @@ export default class SplineEditor2 {
|
||||
this.isDragging = false;
|
||||
this.isDraggingAll = false;
|
||||
this.isScalingAll = false;
|
||||
this.isScalingPoint = false;
|
||||
this.scalingPointIndex = -1;
|
||||
this.scalingPointBaseScale = 1.0;
|
||||
this.scalingPointInitialDistance = 0;
|
||||
this.isRotatingAll = false;
|
||||
this.dragStartPos = null;
|
||||
this.initialXDistance = 0;
|
||||
@@ -744,9 +926,24 @@ export default class SplineEditor2 {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isScalingPoint && this.scalingPointIndex >= 0 && this.scalingPointInitialDistance !== 0) {
|
||||
const point = this.points[this.scalingPointIndex];
|
||||
if (point) {
|
||||
const currentXDistance = adjustedX - point.x;
|
||||
const scaleFactor = currentXDistance / this.scalingPointInitialDistance;
|
||||
const dampingFactor = 0.1;
|
||||
const dampedFactor = 1.0 + (scaleFactor - 1.0) * dampingFactor;
|
||||
const newScale = Math.max(0.2, Math.min(3.0, this.scalingPointBaseScale * dampedFactor));
|
||||
point.boxScale = newScale;
|
||||
point.scale = newScale;
|
||||
this.layerRenderer.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isScalingAll && this.anchorPoint && this.originalPoints && this.initialXDistance !== 0) {
|
||||
const currentXDistance = adjustedX - this.anchorPoint.x;
|
||||
const scaleFactor = currentXDistance / this.initialXDistance;
|
||||
const currentXDistance = adjustedX - this.anchorPoint.x;
|
||||
const scaleFactor = currentXDistance / this.initialXDistance;
|
||||
const dampingFactor = 0.1;
|
||||
const dampedScaleFactor = 1.0 + (scaleFactor - 1.0) * dampingFactor;
|
||||
const clampedScaleFactor = Math.max(0.1, Math.min(10, dampedScaleFactor));
|
||||
@@ -807,7 +1004,7 @@ export default class SplineEditor2 {
|
||||
// Helper to get render points with duplicates at highlighted positions
|
||||
// This forces hard corners while keeping the curve continuous
|
||||
this.getRenderPoints = () => {
|
||||
if (this.interpolation === 'linear' || this.interpolation === 'points') {
|
||||
if (this.interpolation === 'linear' || this.interpolation === 'points' || this.interpolation === 'box') {
|
||||
return this.points;
|
||||
}
|
||||
|
||||
@@ -1226,7 +1423,7 @@ export default class SplineEditor2 {
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.value.interpolation === 'points' || !usePathSampling) {
|
||||
if (widget.value.interpolation === 'points' || widget.value.interpolation === 'box' || !usePathSampling) {
|
||||
sampledCoords = controlPoints;
|
||||
} else if (pathElement) {
|
||||
sampledCoords = [];
|
||||
|
||||
@@ -32,6 +32,10 @@ export {
|
||||
};
|
||||
|
||||
//from melmass
|
||||
export const BOX_BASE_RADIUS = 40;
|
||||
export const POINT_BASE_RADIUS = 8;
|
||||
export const BOX_BORDER_BAND = 20;
|
||||
|
||||
export function makeUUID() {
|
||||
let dt = new Date().getTime()
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
@@ -637,8 +641,8 @@ export class InterpolationWidget extends RgthreeBaseWidget {
|
||||
const interpValue = interpWidget ? interpWidget.value : "linear";
|
||||
|
||||
// Interpolation modes - display abbreviated versions
|
||||
const interpModes = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const interpShortNames = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const interpModes = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const interpShortNames = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const interpIndex = interpModes.indexOf(interpValue);
|
||||
const interpDisplayText = interpIndex >= 0 ? interpShortNames[interpIndex] : 'linear';
|
||||
|
||||
@@ -656,7 +660,8 @@ export class InterpolationWidget extends RgthreeBaseWidget {
|
||||
ctx.measureText("cardinal").width,
|
||||
ctx.measureText("linear").width,
|
||||
ctx.measureText("basis").width,
|
||||
ctx.measureText("points").width
|
||||
ctx.measureText("points").width,
|
||||
ctx.measureText("box").width
|
||||
) + 20; // Add padding
|
||||
|
||||
const arrowWidth = 9;
|
||||
@@ -698,7 +703,7 @@ export class InterpolationWidget extends RgthreeBaseWidget {
|
||||
stepInterp(node, step) {
|
||||
const widget = node.widgets?.find(w => w.name === "interpolation");
|
||||
if (widget) {
|
||||
const modes = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const modes = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const currentIndex = modes.indexOf(widget.value);
|
||||
const newIndex = (currentIndex + step + modes.length) % modes.length;
|
||||
widget.value = modes[newIndex];
|
||||
|
||||
@@ -192,8 +192,8 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
|
||||
rposX -= drawNumberWidgetPart.WIDTH_TOTAL + 10;
|
||||
|
||||
// Draw interpolation selector
|
||||
const interpModes = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const interpShortNames = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const interpModes = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const interpShortNames = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const interpIndex = interpModes.indexOf(this.value.interpolation || 'linear');
|
||||
const interpDisplayText = interpIndex >= 0 ? interpShortNames[interpIndex] : 'linear';
|
||||
const interpTextWidth = 70;
|
||||
@@ -340,7 +340,7 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
|
||||
}
|
||||
|
||||
onInterpDec() {
|
||||
const modes = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const modes = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const currentIndex = modes.indexOf(this.value.interpolation || 'linear');
|
||||
const newIndex = (currentIndex - 1 + modes.length) % modes.length;
|
||||
this.value.interpolation = modes[newIndex];
|
||||
@@ -355,7 +355,7 @@ export class PowerSplineWidget extends RgthreeBaseWidget {
|
||||
}
|
||||
|
||||
onInterpInc() {
|
||||
const modes = ['linear', 'cardinal', 'basis', 'points'];
|
||||
const modes = ['linear', 'cardinal', 'basis', 'points', 'box'];
|
||||
const currentIndex = modes.indexOf(this.value.interpolation || 'linear');
|
||||
const newIndex = (currentIndex + 1) % modes.length;
|
||||
this.value.interpolation = modes[newIndex];
|
||||
|
||||
Reference in New Issue
Block a user