mirror of https://github.com/node-red/node-red.git
commit
ebc079730d
|
|
@ -173,6 +173,8 @@ module.exports = function(grunt) {
|
|||
"packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/statusBar.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-constants.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-animator.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-navigator.js",
|
||||
"packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js",
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@
|
|||
"zoom-out": "Zoom out",
|
||||
"zoom-reset": "Reset zoom",
|
||||
"zoom-in": "Zoom in",
|
||||
"zoom-fit": "Zoom to fit",
|
||||
"search-flows": "Search flows",
|
||||
"search-prev": "Previous",
|
||||
"search-next": "Next",
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@
|
|||
"alt-shift-w": "core:show-last-hidden-flow",
|
||||
"ctrl-+": "core:zoom-in",
|
||||
"ctrl--": "core:zoom-out",
|
||||
"ctrl-0": "core:zoom-reset"
|
||||
"ctrl-0": "core:zoom-reset",
|
||||
"ctrl-1": "core:zoom-fit"
|
||||
},
|
||||
"red-ui-editor-stack": {
|
||||
"ctrl-enter": "core:confirm-edit-tray",
|
||||
|
|
|
|||
|
|
@ -15,139 +15,202 @@
|
|||
**/
|
||||
|
||||
|
||||
RED.view.navigator = (function() {
|
||||
|
||||
var nav_scale = 50;
|
||||
var nav_width = 8000/nav_scale;
|
||||
var nav_height = 8000/nav_scale;
|
||||
|
||||
var navContainer;
|
||||
var navBox;
|
||||
var navBorder;
|
||||
var navVis;
|
||||
var scrollPos;
|
||||
var scaleFactor;
|
||||
var chartSize;
|
||||
var dimensions;
|
||||
var isDragging;
|
||||
var isShowing = false;
|
||||
|
||||
function refreshNodes() {
|
||||
if (!isShowing) {
|
||||
return;
|
||||
}
|
||||
var navNode = navVis.selectAll(".red-ui-navigator-node").data(RED.view.getActiveNodes(),function(d){return d.id});
|
||||
navNode.exit().remove();
|
||||
navNode.enter().insert("rect")
|
||||
.attr('class','red-ui-navigator-node')
|
||||
.attr("pointer-events", "none");
|
||||
navNode.each(function(d) {
|
||||
d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
|
||||
.attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
|
||||
.attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
|
||||
.attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
|
||||
.attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
|
||||
});
|
||||
}
|
||||
function onScroll() {
|
||||
if (!isDragging) {
|
||||
resizeNavBorder();
|
||||
}
|
||||
}
|
||||
function resizeNavBorder() {
|
||||
if (navBorder) {
|
||||
scaleFactor = RED.view.scale();
|
||||
chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
|
||||
scrollPos = [$("#red-ui-workspace-chart").scrollLeft(),$("#red-ui-workspace-chart").scrollTop()];
|
||||
navBorder.attr('x',scrollPos[0]/nav_scale)
|
||||
.attr('y',scrollPos[1]/nav_scale)
|
||||
.attr('width',chartSize[0]/nav_scale/scaleFactor)
|
||||
.attr('height',chartSize[1]/nav_scale/scaleFactor)
|
||||
}
|
||||
}
|
||||
function toggle() {
|
||||
if (!isShowing) {
|
||||
isShowing = true;
|
||||
$("#red-ui-view-navigate").addClass("selected");
|
||||
resizeNavBorder();
|
||||
refreshNodes();
|
||||
$("#red-ui-workspace-chart").on("scroll",onScroll);
|
||||
navContainer.fadeIn(200);
|
||||
} else {
|
||||
isShowing = false;
|
||||
navContainer.fadeOut(100);
|
||||
$("#red-ui-workspace-chart").off("scroll",onScroll);
|
||||
$("#red-ui-view-navigate").removeClass("selected");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init: function() {
|
||||
|
||||
$(window).on("resize", resizeNavBorder);
|
||||
RED.events.on("sidebar:resize",resizeNavBorder);
|
||||
RED.actions.add("core:toggle-navigator",toggle);
|
||||
var hideTimeout;
|
||||
|
||||
navContainer = $('<div>').css({
|
||||
"position":"absolute",
|
||||
"bottom":$("#red-ui-workspace-footer").height(),
|
||||
"right":0,
|
||||
zIndex: 1
|
||||
}).appendTo("#red-ui-workspace").hide();
|
||||
|
||||
navBox = d3.select(navContainer[0])
|
||||
.append("svg:svg")
|
||||
.attr("width", nav_width)
|
||||
.attr("height", nav_height)
|
||||
.attr("pointer-events", "all")
|
||||
.attr("id","red-ui-navigator-canvas")
|
||||
|
||||
navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
|
||||
fill:"none",
|
||||
stroke:"none",
|
||||
pointerEvents:"all"
|
||||
}).on("mousedown", function() {
|
||||
// Update these in case they have changed
|
||||
scaleFactor = RED.view.scale();
|
||||
chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
|
||||
dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
|
||||
var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
|
||||
var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
|
||||
navBorder.attr('x',newX).attr('y',newY);
|
||||
isDragging = true;
|
||||
$("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
|
||||
$("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
|
||||
}).on("mousemove", function() {
|
||||
if (!isDragging) { return }
|
||||
if (d3.event.buttons === 0) {
|
||||
isDragging = false;
|
||||
return;
|
||||
}
|
||||
var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
|
||||
var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
|
||||
navBorder.attr('x',newX).attr('y',newY);
|
||||
$("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
|
||||
$("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
|
||||
}).on("mouseup", function() {
|
||||
isDragging = false;
|
||||
})
|
||||
|
||||
navBorder = navBox.append("rect").attr("class","red-ui-navigator-border")
|
||||
|
||||
navVis = navBox.append("svg:g")
|
||||
|
||||
RED.statusBar.add({
|
||||
id: "view-navigator",
|
||||
align: "right",
|
||||
element: $('<button class="red-ui-footer-button-toggle single" id="red-ui-view-navigate"><i class="fa fa-map-o"></i></button>')
|
||||
})
|
||||
RED.view.navigator = (function() {
|
||||
var nav_scale = 50;
|
||||
var nav_width = 8000/nav_scale;
|
||||
var nav_height = 8000/nav_scale;
|
||||
var navContainer;
|
||||
var navBox;
|
||||
var navBorder;
|
||||
var navVis;
|
||||
var scrollPos;
|
||||
var scaleFactor;
|
||||
var chartSize;
|
||||
var dimensions;
|
||||
var isDragging;
|
||||
var isShowing = false;
|
||||
var toggleTimeout;
|
||||
var autoHideTimeout;
|
||||
var isManuallyToggled = false;
|
||||
var isTemporaryShow = false;
|
||||
function refreshNodes() {
|
||||
if (!isShowing) {
|
||||
return;
|
||||
}
|
||||
var navNode = navVis.selectAll(".red-ui-navigator-node").data(RED.view.getActiveNodes(),function(d){return d.id});
|
||||
navNode.exit().remove();
|
||||
navNode.enter().insert("rect")
|
||||
.attr('class','red-ui-navigator-node')
|
||||
.attr("pointer-events", "none");
|
||||
navNode.each(function(d) {
|
||||
d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
|
||||
.attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
|
||||
.attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
|
||||
.attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
|
||||
.attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
|
||||
});
|
||||
}
|
||||
function onScroll() {
|
||||
if (!isDragging) {
|
||||
resizeNavBorder();
|
||||
}
|
||||
}
|
||||
function resizeNavBorder() {
|
||||
if (navBorder) {
|
||||
scaleFactor = RED.view.scale();
|
||||
chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
|
||||
scrollPos = [$("#red-ui-workspace-chart").scrollLeft(),$("#red-ui-workspace-chart").scrollTop()];
|
||||
// Convert scroll position (in scaled pixels) to workspace coordinates, then to minimap coordinates
|
||||
// scrollPos is in scaled canvas pixels, divide by scaleFactor to get workspace coords
|
||||
navBorder.attr('x',scrollPos[0]/scaleFactor/nav_scale)
|
||||
.attr('y',scrollPos[1]/scaleFactor/nav_scale)
|
||||
.attr('width',chartSize[0]/nav_scale/scaleFactor)
|
||||
.attr('height',chartSize[1]/nav_scale/scaleFactor)
|
||||
}
|
||||
}
|
||||
function show () {
|
||||
if (!isShowing) {
|
||||
isShowing = true;
|
||||
clearTimeout(autoHideTimeout);
|
||||
$("#red-ui-view-navigate").addClass("selected");
|
||||
resizeNavBorder();
|
||||
refreshNodes();
|
||||
$("#red-ui-workspace-chart").on("scroll",onScroll);
|
||||
navContainer.addClass('red-ui-navigator-container');
|
||||
navContainer.show();
|
||||
clearTimeout(toggleTimeout)
|
||||
toggleTimeout = setTimeout(function() {
|
||||
navContainer.addClass('red-ui-navigator-visible');
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
function hide () {
|
||||
if (isShowing) {
|
||||
isShowing = false;
|
||||
isTemporaryShow = false;
|
||||
isManuallyToggled = false;
|
||||
clearTimeout(autoHideTimeout);
|
||||
navContainer.removeClass('red-ui-navigator-visible');
|
||||
clearTimeout(toggleTimeout)
|
||||
toggleTimeout = setTimeout(function() {
|
||||
navContainer.hide();
|
||||
}, 300);
|
||||
$("#red-ui-workspace-chart").off("scroll",onScroll);
|
||||
$("#red-ui-view-navigate").removeClass("selected");
|
||||
}
|
||||
}
|
||||
function toggle() {
|
||||
if (!isShowing) {
|
||||
isManuallyToggled = true;
|
||||
show()
|
||||
} else {
|
||||
isManuallyToggled = false;
|
||||
hide()
|
||||
}
|
||||
}
|
||||
function setupAutoHide () {
|
||||
clearTimeout(autoHideTimeout);
|
||||
autoHideTimeout = setTimeout(function() {
|
||||
hide()
|
||||
}, 2000)
|
||||
}
|
||||
function showTemporary() {
|
||||
if (!isManuallyToggled) {
|
||||
isTemporaryShow = true
|
||||
clearTimeout(autoHideTimeout);
|
||||
show()
|
||||
setupAutoHide()
|
||||
}
|
||||
}
|
||||
return {
|
||||
init: function() {
|
||||
$(window).on("resize", resizeNavBorder);
|
||||
RED.events.on("sidebar:resize",resizeNavBorder);
|
||||
RED.actions.add("core:toggle-navigator",toggle);
|
||||
navContainer = $('<div>').css({
|
||||
"position":"absolute",
|
||||
"bottom":$("#red-ui-workspace-footer").height(),
|
||||
"right":0,
|
||||
zIndex: 1
|
||||
}).addClass('red-ui-navigator-container').appendTo("#red-ui-workspace").hide();
|
||||
navBox = d3.select(navContainer[0])
|
||||
.append("svg:svg")
|
||||
.attr("width", nav_width)
|
||||
.attr("height", nav_height)
|
||||
.attr("pointer-events", "all")
|
||||
.attr("id","red-ui-navigator-canvas")
|
||||
navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
|
||||
fill:"none",
|
||||
stroke:"none",
|
||||
pointerEvents:"all"
|
||||
}).on("mousedown", function() {
|
||||
// Update these in case they have changed
|
||||
scaleFactor = RED.view.scale();
|
||||
chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
|
||||
dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
|
||||
var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
|
||||
var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
|
||||
navBorder.attr('x',newX).attr('y',newY);
|
||||
isDragging = true;
|
||||
$("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
|
||||
$("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
|
||||
}).on("mousemove", function() {
|
||||
if (!isDragging) { return }
|
||||
if (d3.event.buttons === 0) {
|
||||
isDragging = false;
|
||||
return;
|
||||
}
|
||||
var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
|
||||
var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
|
||||
navBorder.attr('x',newX).attr('y',newY);
|
||||
$("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
|
||||
$("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
|
||||
}).on("mouseup", function() {
|
||||
isDragging = false;
|
||||
}).on("mouseenter", function () {
|
||||
if (isTemporaryShow) {
|
||||
// If user hovers over the minimap while it's temporarily shown, keep it shown
|
||||
clearTimeout(autoHideTimeout);
|
||||
}
|
||||
}).on("mouseleave", function () {
|
||||
if (isTemporaryShow) {
|
||||
// Restart the auto-hide timer after mouse leaves the minimap
|
||||
setupAutoHide()
|
||||
}
|
||||
})
|
||||
navBorder = navBox.append("rect").attr("class","red-ui-navigator-border")
|
||||
navVis = navBox.append("svg:g")
|
||||
RED.statusBar.add({
|
||||
id: "view-navigator",
|
||||
align: "right",
|
||||
element: $('<button class="red-ui-footer-button-toggle single" id="red-ui-view-navigate"><i class="fa fa-map-o"></i></button>')
|
||||
})
|
||||
|
||||
$("#red-ui-view-navigate").on("click", function(evt) {
|
||||
evt.preventDefault();
|
||||
toggle();
|
||||
})
|
||||
RED.popover.tooltip($("#red-ui-view-navigate"),RED._('actions.toggle-navigator'),'core:toggle-navigator');
|
||||
|
||||
// Listen for canvas interactions to show minimap temporarily
|
||||
// Only show on actual pan/zoom navigation, not selection changes
|
||||
// RED.events.on("view:navigate", function() {
|
||||
// showTemporary();
|
||||
// });
|
||||
|
||||
// Show minimap briefly when workspace changes (includes initial load)
|
||||
// RED.events.on("workspace:change", function(event) {
|
||||
// // Only show if there's an active workspace with nodes
|
||||
// if (event.workspace && RED.nodes.getWorkspaceOrder().length > 0) {
|
||||
// // Small delay to ensure nodes are rendered
|
||||
// setTimeout(function() {
|
||||
// var activeNodes = RED.nodes.filterNodes({z: event.workspace});
|
||||
// if (activeNodes.length > 0) {
|
||||
// showTemporary();
|
||||
// }
|
||||
// }, 100);
|
||||
// }
|
||||
// });
|
||||
},
|
||||
refresh: refreshNodes,
|
||||
resize: resizeNavBorder,
|
||||
|
|
|
|||
257
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-animator.js
vendored
Normal file
257
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-animator.js
vendored
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
RED.view.zoomAnimator = (function() {
|
||||
|
||||
/**
|
||||
* Easing function for smooth deceleration
|
||||
* Creates natural-feeling animation curves
|
||||
* @param {number} t - Progress from 0 to 1
|
||||
* @returns {number} - Eased value from 0 to 1
|
||||
*/
|
||||
function easeOut(t) {
|
||||
// Cubic ease-out for smooth deceleration
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate values using requestAnimationFrame with easing
|
||||
* Based on Excalidraw's implementation for smooth zoom transitions
|
||||
*
|
||||
* @param {Object} options - Animation options
|
||||
* @param {Object} options.fromValues - Starting values object
|
||||
* @param {Object} options.toValues - Target values object
|
||||
* @param {Function} options.onStep - Callback for each animation frame
|
||||
* @param {number} [options.duration=250] - Animation duration in ms
|
||||
* @param {Function} [options.interpolateValue] - Custom interpolation function
|
||||
* @param {Function} [options.onStart] - Animation start callback
|
||||
* @param {Function} [options.onEnd] - Animation end callback
|
||||
* @param {Function} [options.onCancel] - Animation cancel callback
|
||||
* @returns {Function} - Cancel function to stop animation
|
||||
*/
|
||||
function easeToValuesRAF(options) {
|
||||
const {
|
||||
fromValues,
|
||||
toValues,
|
||||
onStep,
|
||||
duration = 250,
|
||||
interpolateValue,
|
||||
onStart,
|
||||
onEnd,
|
||||
onCancel
|
||||
} = options;
|
||||
|
||||
let startTime = null;
|
||||
let animationId = null;
|
||||
let cancelled = false;
|
||||
|
||||
function step(timestamp) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTime) {
|
||||
startTime = timestamp;
|
||||
if (onStart) {
|
||||
onStart();
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = timestamp - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easedProgress = easeOut(progress);
|
||||
|
||||
const interpolatedValues = {};
|
||||
|
||||
for (const key in fromValues) {
|
||||
if (fromValues.hasOwnProperty(key)) {
|
||||
const from = fromValues[key];
|
||||
const to = toValues[key];
|
||||
|
||||
if (interpolateValue && key === 'zoom') {
|
||||
// Special interpolation for zoom to feel more natural
|
||||
// Exponential interpolation preserves relative zoom feel
|
||||
interpolatedValues[key] = from * Math.pow(to / from, easedProgress);
|
||||
} else {
|
||||
// Linear interpolation for other values
|
||||
interpolatedValues[key] = from + (to - from) * easedProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onStep(interpolatedValues);
|
||||
|
||||
if (progress < 1) {
|
||||
animationId = requestAnimationFrame(step);
|
||||
} else {
|
||||
if (onEnd) {
|
||||
onEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(step);
|
||||
|
||||
// Return cancel function
|
||||
return function cancel() {
|
||||
cancelled = true;
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate smooth zoom delta with acceleration
|
||||
* Provides consistent zoom speed regardless of input device
|
||||
*
|
||||
* @param {number} currentScale - Current zoom scale
|
||||
* @param {number} delta - Input delta (wheel, gesture, etc)
|
||||
* @param {boolean} isTrackpad - Whether input is from trackpad
|
||||
* @returns {number} - Calculated zoom delta
|
||||
*/
|
||||
function calculateZoomDelta(currentScale, delta, isTrackpad) {
|
||||
// Normalize delta across different input devices
|
||||
let normalizedDelta = delta;
|
||||
|
||||
if (isTrackpad) {
|
||||
// Trackpad deltas are typically smaller and more frequent
|
||||
normalizedDelta = delta * 0.005; // Reduced from 0.01 for gentler zoom
|
||||
} else {
|
||||
// Mouse wheel deltas are larger and less frequent
|
||||
// Reduce zoom out speed more than zoom in
|
||||
normalizedDelta = delta > 0 ? 0.06 : -0.08; // Reduced from 0.1, asymmetric for gentler zoom out
|
||||
}
|
||||
|
||||
// Apply gentler acceleration based on current zoom level
|
||||
// Less aggressive acceleration to prevent rapid zoom out
|
||||
const acceleration = Math.max(0.7, Math.min(1.1, 1 / currentScale)); // Reduced from 0.5-1.2 to 0.7-1.1
|
||||
|
||||
return normalizedDelta * acceleration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gesture state management for consistent focal points
|
||||
*/
|
||||
const gestureState = {
|
||||
active: false,
|
||||
initialFocalPoint: null, // Will store workspace coordinates
|
||||
initialScale: 1,
|
||||
currentScale: 1,
|
||||
lastDistance: 0,
|
||||
scrollPosAtStart: null, // Store initial scroll position
|
||||
scaleFatorAtStart: 1 // Store initial scale factor
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a zoom gesture with fixed focal point
|
||||
* @param {Array} focalPoint - [x, y] coordinates of focal point in workspace
|
||||
* @param {number} scale - Initial scale value
|
||||
* @param {Array} scrollPos - Current scroll position [x, y]
|
||||
* @param {number} currentScaleFactor - Current scale factor for coordinate conversion
|
||||
*/
|
||||
function startGesture(focalPoint, scale, scrollPos, currentScaleFactor) {
|
||||
gestureState.active = true;
|
||||
// Store the focal point in workspace coordinates for stability
|
||||
// This ensures the point remains fixed even if scroll changes due to canvas edge constraints
|
||||
if (focalPoint && scrollPos && currentScaleFactor) {
|
||||
gestureState.initialFocalPoint = [
|
||||
(scrollPos[0] + focalPoint[0]) / currentScaleFactor,
|
||||
(scrollPos[1] + focalPoint[1]) / currentScaleFactor
|
||||
];
|
||||
gestureState.scrollPosAtStart = [...scrollPos];
|
||||
gestureState.scaleFatorAtStart = currentScaleFactor;
|
||||
} else {
|
||||
gestureState.initialFocalPoint = focalPoint ? [...focalPoint] : null;
|
||||
}
|
||||
gestureState.initialScale = scale;
|
||||
gestureState.currentScale = scale;
|
||||
return gestureState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update gesture maintaining fixed focal point
|
||||
* @param {number} newScale - New scale value
|
||||
* @returns {Object} - Gesture state with fixed focal point
|
||||
*/
|
||||
function updateGesture(newScale) {
|
||||
if (!gestureState.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
gestureState.currentScale = newScale;
|
||||
|
||||
return {
|
||||
scale: newScale,
|
||||
focalPoint: gestureState.initialFocalPoint,
|
||||
active: gestureState.active
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current gesture
|
||||
*/
|
||||
function endGesture() {
|
||||
gestureState.active = false;
|
||||
gestureState.initialFocalPoint = null;
|
||||
gestureState.lastDistance = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a gesture is currently active
|
||||
*/
|
||||
function isGestureActive() {
|
||||
return gestureState.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fixed focal point for the current gesture
|
||||
* @param {Array} currentScrollPos - Current scroll position [x, y]
|
||||
* @param {number} currentScaleFactor - Current scale factor
|
||||
* @returns {Array} - Focal point in screen coordinates or null
|
||||
*/
|
||||
function getGestureFocalPoint(currentScrollPos, currentScaleFactor) {
|
||||
if (!gestureState.initialFocalPoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we stored workspace coordinates, convert back to screen coordinates
|
||||
if (gestureState.scrollPosAtStart && currentScrollPos && currentScaleFactor) {
|
||||
// Convert workspace coordinates back to current screen coordinates
|
||||
return [
|
||||
gestureState.initialFocalPoint[0] * currentScaleFactor - currentScrollPos[0],
|
||||
gestureState.initialFocalPoint[1] * currentScaleFactor - currentScrollPos[1]
|
||||
];
|
||||
}
|
||||
|
||||
return gestureState.initialFocalPoint;
|
||||
}
|
||||
|
||||
return {
|
||||
easeOut: easeOut,
|
||||
easeToValuesRAF: easeToValuesRAF,
|
||||
calculateZoomDelta: calculateZoomDelta,
|
||||
gestureState: gestureState,
|
||||
startGesture: startGesture,
|
||||
updateGesture: updateGesture,
|
||||
endGesture: endGesture,
|
||||
isGestureActive: isGestureActive,
|
||||
getGestureFocalPoint: getGestureFocalPoint
|
||||
};
|
||||
})();
|
||||
21
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-constants.js
vendored
Normal file
21
packages/node_modules/@node-red/editor-client/src/js/ui/view-zoom-constants.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Zoom configuration constants
|
||||
*/
|
||||
RED.view.zoomConstants = {
|
||||
// Zoom limits
|
||||
MIN_ZOOM: 0.05, // Default minimum, will be dynamically calculated to fit canvas
|
||||
MAX_ZOOM: 2.0,
|
||||
|
||||
// Zoom step for keyboard/button controls
|
||||
ZOOM_STEP: 0.2,
|
||||
|
||||
// Animation settings
|
||||
DEFAULT_ZOOM_DURATION: 125, // ms, faster animation
|
||||
|
||||
// Gesture thresholds
|
||||
PINCH_THRESHOLD: 10, // minimum pixel movement to trigger zoom
|
||||
|
||||
// Momentum and friction for smooth scrolling
|
||||
FRICTION: 0.92,
|
||||
BOUNCE_DAMPING: 0.6
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -17,8 +17,9 @@
|
|||
**/
|
||||
|
||||
|
||||
body {
|
||||
html, body {
|
||||
overflow: hidden;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.red-ui-editor {
|
||||
|
|
@ -29,6 +30,7 @@ body {
|
|||
background: var(--red-ui-primary-background);
|
||||
color: var(--red-ui-primary-text-color);
|
||||
line-height: 20px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
#red-ui-editor {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,25 @@
|
|||
right:0px;
|
||||
box-sizing:border-box;
|
||||
transition: right 0.2s ease;
|
||||
touch-action: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
// Hide scrollbars - to be done in a future iteration
|
||||
// scrollbar-width: none; /* Firefox */
|
||||
// -ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
// &::-webkit-scrollbar { /* WebKit */
|
||||
// width: 0;
|
||||
// height: 0;
|
||||
// }
|
||||
|
||||
// Reset SVG default margins
|
||||
> svg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -150,6 +169,15 @@
|
|||
background: var(--red-ui-view-navigator-background);
|
||||
box-shadow: -1px 0 3px var(--red-ui-shadow);
|
||||
}
|
||||
|
||||
.red-ui-navigator-container {
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.red-ui-navigator-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.red-ui-navigator-border {
|
||||
stroke-dasharray: 5,5;
|
||||
pointer-events: none;
|
||||
|
|
|
|||
Loading…
Reference in New Issue