From 887250a969b6e5eb746b66858c714d7bbf0bac10 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Wed, 30 Jan 2019 00:48:29 -0500 Subject: [PATCH 01/10] POC for dragging a selection box on the canvas --- lib/network/modules/Canvas.js | 1 + lib/network/modules/CanvasRenderer.js | 14 ++++++ lib/network/modules/InteractionHandler.js | 56 +++++++++++++++++++++-- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index 753e56cb9..b64ed8380 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -254,6 +254,7 @@ class Canvas { this.frame.canvas.addEventListener('mousemove', (event) => {this.body.eventListeners.onMouseMove(event)}); this.frame.canvas.addEventListener('contextmenu', (event) => {this.body.eventListeners.onContext(event)}); + this.frame.canvas.addEventListener('mousedown', (event) => {this.body.eventListeners.onMouseDown(event)}); this.hammerFrame = new Hammer(this.frame); hammerUtil.onRelease(this.hammerFrame, (event) => {this.body.eventListeners.onRelease(event)}); diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index 56b9e5183..407fce03f 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -273,6 +273,9 @@ class CanvasRenderer { this._drawNodes(ctx, hidden); } + // if it should draw selection box: + this._drawSelectionBox(ctx); + ctx.beginPath(); this.body.emitter.emit("afterDrawing", ctx); ctx.closePath(); @@ -286,6 +289,17 @@ class CanvasRenderer { } } + _drawSelectionBox(ctx /* x0, y0, x1, y1 */) { + ctx.save(); + { + ctx.rect(this.body.selectionBox.x, this.body.selectionBox.y, this.body.selectionBox.width, this.body.selectionBox.height); + ctx.stroke(); + + //ctx.fillStyle = "rgba(0, 0, 0, 0.25)"; + //ctx.fillRect(canvasRectModelCoords.x + 1, canvasRectModelCoords.y + 1, 98, 98); + } + ctx.restore(); + } /** * Redraw all nodes diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index cbdc49013..d5a1816a8 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -32,9 +32,11 @@ class InteractionHandler { this.body.eventListeners.onRelease = this.onRelease.bind(this); this.body.eventListeners.onContext = this.onContext.bind(this); + this.body.selectionBox = {}; + this.touchTime = 0; this.drag = {}; - this.pinch = {}; + this.pinch = {}; this.popup = undefined; this.popupObj = undefined; this.popupTimer = undefined; @@ -95,6 +97,15 @@ class InteractionHandler { this.navigationHandler.setOptions(this.options); } + isInSelectionBoxState() { + if (Object.keys(this.body.selectionBox).length > 0) { + return true; + } + else { + return false; + } + } + /** * Get the pointer location from a touch location @@ -109,17 +120,34 @@ class InteractionHandler { }; } - /** * On start of a touch gesture, store the pointer + * note for selectionBox functionality -- this Hammer event consumes the "mousedown" DOM event, so we deal with it here * @param {Event} event The event * @private */ - onTouch(event) { + onTouch(event) { if (new Date().valueOf() - this.touchTime > 50) { + if (event.srcEvent.ctrlKey) { + console.log(event); + let p = this.canvas.DOMtoCanvas({ + x: event.srcEvent.offsetX, + y: event.srcEvent.offsetY + }); + + this.body.selectionBox["x"] = p.x; + this.body.selectionBox["y"] = p.y; + + this.body.selectionBox["width"] = 0; + this.body.selectionBox["height"] = 0; + + console.log("SELECTION BOX STATE: " + this.isInSelectionBoxState()); + } + this.drag.pointer = this.getPointer(event.center); this.drag.pinched = false; this.pinch.scale = this.body.view.scale; + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) this.touchTime = new Date().valueOf(); } @@ -176,6 +204,10 @@ class InteractionHandler { */ onRelease(event) { if (new Date().valueOf() - this.touchTime > 10) { + if (this.isInSelectionBoxState()) { + console.log("Clearing selection box state"); + this.body.selectionBox = {}; + } let pointer = this.getPointer(event.center); this.selectionHandler._generateClickEvent('release', event, pointer); // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) @@ -351,6 +383,24 @@ class InteractionHandler { return; } + if (this.isInSelectionBoxState()) { + console.log("MUTATING SELECTION BOX"); + let p = this.canvas.DOMtoCanvas({ + x: event.srcEvent.offsetX, + y: event.srcEvent.offsetY + }); + + let xDir = this.body.selectionBox.x <= p.x ? 1 : -1; + let yDir = this.body.selectionBox.y <= p.y ? 1 : -1; + + this.body.selectionBox["width"] = Math.abs(p.x - this.body.selectionBox.x) * xDir; + this.body.selectionBox["height"] = Math.abs(p.y - this.body.selectionBox.y) * yDir; + //console.log("ORIGIN: " + this.body.selectionBox.x + ", " + this.body.selectionBox.y + " | nx: " + (p.x * xDir) + ", ny: " + (p.y * yDir)); + this.body.emitter.emit("_redraw"); + + return; + } + // remove the focus on node if it is focussed on by the focusOnNode this.body.emitter.emit('unlockNode'); From e9fa3582b8c63eb4698545687d5ab21822756c23 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Wed, 30 Jan 2019 23:34:26 -0500 Subject: [PATCH 02/10] Break out functionality into seperate class --- lib/network/Network.js | 6 +- lib/network/modules/CanvasRenderer.js | 15 ++- lib/network/modules/InteractionHandler.js | 63 ++++----- lib/network/modules/SelectionBox.js | 123 ++++++++++++++++++ lib/network/modules/SelectionHandler.js | 24 +++- lib/network/modules/components/Edge.js | 57 +++++++- .../modules/components/NavigationHandler.js | 1 - lib/network/options.js | 7 + lib/util.js | 10 ++ 9 files changed, 253 insertions(+), 53 deletions(-) create mode 100644 lib/network/modules/SelectionBox.js diff --git a/lib/network/Network.js b/lib/network/Network.js index f0f3a240c..905daf244 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -27,6 +27,7 @@ var {printStyle} = require('./../shared/Validator'); var {allOptions, configureOptions} = require('./options.js'); var KamadaKawai = require("./modules/KamadaKawai.js").default; +var SelectionBox = require('./modules/SelectionBox').default; /** * Create a network visualization, displaying nodes and edges. @@ -120,9 +121,10 @@ function Network(container, data, options) { this.groups = new Groups(); // object with groups this.canvas = new Canvas(this.body); // DOM handler this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler - this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key + this.selectionBox = new SelectionBox(this.body, this.selectionHandler); + this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler, this.selectionBox); // Interaction handler handles all the hammer bindings (that are bound by canvas), key this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms - this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into + this.renderer = new CanvasRenderer(this.body, this.canvas, this.selectionBox); // renderer, starts renderloop, has events that modules can hook into this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout this.clustering = new ClusterEngine(this.body); // clustering api diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index 407fce03f..a29b32556 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -51,10 +51,11 @@ class CanvasRenderer { * @param {Object} body * @param {Canvas} canvas */ - constructor(body, canvas) { + constructor(body, canvas, selectionBox) { _initRequestAnimationFrame(); this.body = body; this.canvas = canvas; + this.selectionBox = selectionBox; this.redrawRequested = false; this.renderTimer = undefined; @@ -274,7 +275,9 @@ class CanvasRenderer { } // if it should draw selection box: - this._drawSelectionBox(ctx); + if (this.selectionBox.isActive()) { + this._drawSelectionBox(ctx); + } ctx.beginPath(); this.body.emitter.emit("afterDrawing", ctx); @@ -289,14 +292,14 @@ class CanvasRenderer { } } - _drawSelectionBox(ctx /* x0, y0, x1, y1 */) { + _drawSelectionBox(ctx) { ctx.save(); { - ctx.rect(this.body.selectionBox.x, this.body.selectionBox.y, this.body.selectionBox.width, this.body.selectionBox.height); + ctx.rect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height); ctx.stroke(); - //ctx.fillStyle = "rgba(0, 0, 0, 0.25)"; - //ctx.fillRect(canvasRectModelCoords.x + 1, canvasRectModelCoords.y + 1, 98, 98); + ctx.fillStyle = "rgba(0, 0, 0, 0.0625)"; + ctx.fillRect(this.selectionBox.x + 1, this.selectionBox.y + 1, this.selectionBox.width - 2, this.selectionBox.height - 2); } ctx.restore(); } diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index d5a1816a8..7cac49d3c 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -1,6 +1,7 @@ let util = require('../../util'); var NavigationHandler = require('./components/NavigationHandler').default; var Popup = require('./../../shared/Popup').default; +var SelectionBox = require('./SelectionBox').default; /** @@ -11,12 +12,14 @@ class InteractionHandler { * @param {Object} body * @param {Canvas} canvas * @param {SelectionHandler} selectionHandler + * @param {SelectionBox} selectionBox */ - constructor(body, canvas, selectionHandler) { + constructor(body, canvas, selectionHandler, selectionBox) { this.body = body; this.canvas = canvas; this.selectionHandler = selectionHandler; this.navigationHandler = new NavigationHandler(body,canvas); + this.selectionBox = selectionBox; // bind the events from hammer to functions in this object this.body.eventListeners.onTap = this.onTap.bind(this); @@ -32,8 +35,6 @@ class InteractionHandler { this.body.eventListeners.onRelease = this.onRelease.bind(this); this.body.eventListeners.onContext = this.onContext.bind(this); - this.body.selectionBox = {}; - this.touchTime = 0; this.drag = {}; this.pinch = {}; @@ -63,6 +64,11 @@ class InteractionHandler { this.bindEventListeners() } + _selectionBoxOption() { + return Boolean(this.selectionHandler.options.selectionBox) + //return true; + } + /** * Binds event listeners */ @@ -97,16 +103,6 @@ class InteractionHandler { this.navigationHandler.setOptions(this.options); } - isInSelectionBoxState() { - if (Object.keys(this.body.selectionBox).length > 0) { - return true; - } - else { - return false; - } - } - - /** * Get the pointer location from a touch location * @param {{x: number, y: number}} touch @@ -128,20 +124,14 @@ class InteractionHandler { */ onTouch(event) { if (new Date().valueOf() - this.touchTime > 50) { - if (event.srcEvent.ctrlKey) { - console.log(event); - let p = this.canvas.DOMtoCanvas({ - x: event.srcEvent.offsetX, - y: event.srcEvent.offsetY - }); - - this.body.selectionBox["x"] = p.x; - this.body.selectionBox["y"] = p.y; - - this.body.selectionBox["width"] = 0; - this.body.selectionBox["height"] = 0; - - console.log("SELECTION BOX STATE: " + this.isInSelectionBoxState()); + if (this._selectionBoxOption()) { + if (event.srcEvent.ctrlKey) { + let p = this.canvas.DOMtoCanvas({ + x: event.srcEvent.offsetX, + y: event.srcEvent.offsetY + }); + this.selectionBox.activate(p); + } } this.drag.pointer = this.getPointer(event.center); @@ -196,6 +186,7 @@ class InteractionHandler { } + /** * handle the release of the screen * @@ -204,9 +195,8 @@ class InteractionHandler { */ onRelease(event) { if (new Date().valueOf() - this.touchTime > 10) { - if (this.isInSelectionBoxState()) { - console.log("Clearing selection box state"); - this.body.selectionBox = {}; + if (this.selectionBox.isActive()) { + this.selectionBox.complete(); } let pointer = this.getPointer(event.center); this.selectionHandler._generateClickEvent('release', event, pointer); @@ -383,20 +373,13 @@ class InteractionHandler { return; } - if (this.isInSelectionBoxState()) { - console.log("MUTATING SELECTION BOX"); + if (this.selectionBox.isActive()) { let p = this.canvas.DOMtoCanvas({ x: event.srcEvent.offsetX, y: event.srcEvent.offsetY }); - - let xDir = this.body.selectionBox.x <= p.x ? 1 : -1; - let yDir = this.body.selectionBox.y <= p.y ? 1 : -1; - - this.body.selectionBox["width"] = Math.abs(p.x - this.body.selectionBox.x) * xDir; - this.body.selectionBox["height"] = Math.abs(p.y - this.body.selectionBox.y) * yDir; - //console.log("ORIGIN: " + this.body.selectionBox.x + ", " + this.body.selectionBox.y + " | nx: " + (p.x * xDir) + ", ny: " + (p.y * yDir)); - this.body.emitter.emit("_redraw"); + + this.selectionBox.updateBoundingBox(p) return; } diff --git a/lib/network/modules/SelectionBox.js b/lib/network/modules/SelectionBox.js new file mode 100644 index 000000000..10bb19a4f --- /dev/null +++ b/lib/network/modules/SelectionBox.js @@ -0,0 +1,123 @@ +class SelectionBox { + constructor(body, selectionHandler) { + this.body = body; + this.selectionHandler = selectionHandler; + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + this.active = false; + } + + // + // check to see if the user is currently drawing a selection box + // + isActive() { + return this.active; + } + + // + // activate the selectionBox + // pass in the origin point in canvas space + // p := {x: number, y: number} + // + activate(p) { + console.log(this.selectionHandler); + this.x = p.x; + this.y = p.y; + this.active = true; + } + + deactivate() { + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + this.active = false; + } + + // + // complete the selectionBox, the user has let go of their mouse button + // select the + // + complete() { + let boundingBox = this.getBoundingBox(); + if (this.selectionHandler.options.selectionBox.edges || this.selectionHandler.options.selectionBox === true) { + // run the edges first, so that if "this.selectionHandler.options.selectConnectedEdges" is set, + // we don't _unset_ edges that _get_ set by our node selection logic + let selectedEdgeIds = this.selectionHandler.getAllEdgesWithinBoundingBox(boundingBox); + for (let edgeId of selectedEdgeIds) { + let edge = this.body.edges[edgeId]; + if (edge.isSelected()) { + this.selectionHandler.deselectObject(edge); + } + else { + this.selectionHandler.selectObject(edge); + } + } + } + if (this.selectionHandler.options.selectionBox.nodes || this.selectionHandler.options.selectionBox === true) { + let selectedNodeIds = this.selectionHandler.getAllNodesWithinBoundingBox(boundingBox); + for (let nodeId of selectedNodeIds) { + let node = this.body.nodes[nodeId]; + if (node.isSelected()) { + this.selectionHandler.deselectObject(node); + } + else { + this.selectionHandler.selectObject(node); + } + } + } + this.deactivate(); + } + + // + // get the current bounding box for the user's selection box + // returns {left, top, right, bottom} in canvas model space + // + getBoundingBox() { + let result = { + left: 0, + top: 0, + right: 0, + bottom: 0 + } + + if (this.width < 0) { + result.left = this.x + this.width; + result.right = this.x; + } + else { + result.left = this.x; + result.right = this.x + this.width; + } + + if (this.height < 0) { + result.top = this.y + this.height; + result.bottom = this.y; + } + else { + result.top = this.y; + result.bottom = this.y + this.height; + } + + return result; + } + + // + // update the bounding box as per user mouse movement + // p is an object {x, y} in canvas model space + // new position of mouse is used as new corner of box + // (point at which user began the bounding box remains fixed) + // + updateBoundingBox(p) { + let xDir = this.x <= p.x ? 1 : -1; + let yDir = this.y <= p.y ? 1 : -1; + + this.width = Math.abs(p.x - this.x) * xDir; + this.height = Math.abs(p.y - this.y) * yDir; + this.body.emitter.emit("_redraw"); + } +} + +export default SelectionBox; \ No newline at end of file diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 53c617041..b61bdf429 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -22,7 +22,8 @@ class SelectionHandler { multiselect: false, selectable: true, selectConnectedEdges: true, - hoverConnectedEdges: true + hoverConnectedEdges: true, + selectionBox: true }; util.extend(this.options, this.defaultOptions); @@ -38,7 +39,7 @@ class SelectionHandler { */ setOptions(options) { if (options !== undefined) { - let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges']; + let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges', 'selectionBox']; util.selectiveDeepExtend(fields,this.options, options); } } @@ -202,6 +203,12 @@ class SelectionHandler { return overlappingNodes; } + getAllNodesWithinBoundingBox(object) { + return this._getAllNodesOverlappingWith(object); + } + + + /** * Return a position object in canvasspace from a single point in screenspace @@ -257,7 +264,7 @@ class SelectionHandler { _getEdgesOverlappingWith(object, overlappingEdges) { let edges = this.body.edges; for (let i = 0; i < this.body.edgeIndices.length; i++) { - let edgeId = this.body.edgeIndices[i]; + let edgeId = this.body.edgeIndices[i]; if (edges[edgeId].isOverlappingWith(object)) { overlappingEdges.push(edgeId); } @@ -277,6 +284,17 @@ class SelectionHandler { return overlappingEdges; } + getAllEdgesWithinBoundingBox(object) { + let result = []; + for (let i = 0; i < this.body.edgeIndices.length; i++) { + let edgeId = this.body.edgeIndices[i]; + if (this.body.edges[edgeId].SB_checkBoundingBox(object, 25)) { + result.push(edgeId); + } + } + return result; + } + /** * Get the edges nearest to the passed point (like a click) diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index 2b3d2046c..f00cf834a 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -52,6 +52,22 @@ class Edge { this.setOptions(options); } + SB_checkBoundingBox(boundingBox, points = 5) { + let step = 1 / points; + let percentage = 0; + for (let i = 0; i < points; i++) { + if (i == points - 1) { + percentage = 1; + } + let p = this.edgeType.getPoint(percentage); + if (util.pointWithinBoundingBox(p, boundingBox)) { + return true; + } + percentage += step; + } + return false; + } + /** * Set or overwrite options for the edge @@ -674,10 +690,49 @@ class Edge { return (dist < distMax); } else { - return false + return false; } } + SB_existsWithinBoundingBox(boundingBox) { + let thisBox = { + left: 0, + top: 0, + right: 0, + bottom: 0 + }; + + if (this.from.x < this.to.x) { + thisBox.left = this.from.x; + thisBox.right = this.to.x; + } + else { + thisBox.left = this.to.x; + thisBox.right = this.from.x; + } + + if (this.from.y < this.to.y) { + thisBox.top = this.from.y; + thisBox.bottom = this.to.y; + } + else { + thisBox.top = this.to.y; + thisBox.bottom = this.from.y; + } + + console.log(boundingBox); + console.log(thisBox); + console.log("===="); + if (thisBox.left >= boundingBox.left && + thisBox.right <= boundingBox.right && + thisBox.top >= boundingBox.top && + thisBox.bottom <= boundingBox.bottom) { + return true; + } + else { + return false; + } + } /** * Determine the rotation point, if any. diff --git a/lib/network/modules/components/NavigationHandler.js b/lib/network/modules/components/NavigationHandler.js index 73bec0b43..7672d340a 100644 --- a/lib/network/modules/components/NavigationHandler.js +++ b/lib/network/modules/components/NavigationHandler.js @@ -20,7 +20,6 @@ class NavigationHandler { this.touchTime = 0; this.activated = false; - this.body.emitter.on("activate", () => {this.activated = true; this.configureKeyboardBindings();}); this.body.emitter.on("deactivate", () => {this.activated = false; this.configureKeyboardBindings();}); this.body.emitter.on("destroy", () => {if (this.keycharm !== undefined) {this.keycharm.destroy();}}); diff --git a/lib/network/options.js b/lib/network/options.js index ff614a5df..44d7a1328 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -164,6 +164,12 @@ let allOptions = { navigationButtons: { boolean: bool }, selectable: { boolean: bool }, selectConnectedEdges: { boolean: bool }, + selectionBox: { + enabled: {boolean: bool}, + edges: {boolean: bool }, + nodes: {boolean: bool }, + __type__: { object, boolean: bool } + }, hoverConnectedEdges: { boolean: bool }, tooltipDelay: { number }, zoomView: { boolean: bool }, @@ -571,6 +577,7 @@ let configureOptions = { navigationButtons: false, selectable: true, selectConnectedEdges: true, + selectionBox: true, hoverConnectedEdges: true, tooltipDelay: [300, 0, 1000, 25], zoomView: true, diff --git a/lib/util.js b/lib/util.js index 41549de0f..2203380db 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1566,3 +1566,13 @@ exports.topMost = function (pile, accessors) { } return candidate; }; + +exports.pointWithinBoundingBox = function(point, boundingBox) { + if (point.x >= boundingBox.left && point.x <= boundingBox.right && + point.y >= boundingBox.top && point.y <= boundingBox.bottom) { + return true; + } + else { + return false; + } +} \ No newline at end of file From b0fe7ccd880a5191d603ce3bb8de2438c6803319 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Thu, 31 Jan 2019 23:00:09 -0500 Subject: [PATCH 03/10] fixed selectionBox option handling --- lib/network/modules/InteractionHandler.js | 13 +++++++++++-- lib/network/modules/SelectionHandler.js | 17 +++++++++++++---- lib/network/options.js | 12 ++++++++---- lib/shared/Configurator.js | 12 +++++++++--- lib/util.js | 2 +- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 7cac49d3c..0707bfb8c 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -65,7 +65,7 @@ class InteractionHandler { } _selectionBoxOption() { - return Boolean(this.selectionHandler.options.selectionBox) + return this.selectionHandler.options.selectionBox.enabled; //return true; } @@ -86,7 +86,16 @@ class InteractionHandler { setOptions(options) { if (options !== undefined) { // extend all but the values in fields - let fields = ['hideEdgesOnDrag','hideNodesOnDrag','keyboard','multiselect','selectable','selectConnectedEdges']; + // (there is an overlap between the responsibilites of Network's CanvasRenderer, InteractionHandler and SelectionHandler + let fields = [ + 'hideEdgesOnDrag' /* see CanvasRenderer */, + 'hideNodesOnDrag' /* see CanvasRenderer */, + 'keyboard' /* nested object extended below */, + 'multiselect' /* see SelectionHandler */, + 'selectable' /* see SelectionHandler */, + 'selectConnectedEdges' /* see SelectionHandler */, + 'selectionBox' /* see SelectionHandler*/ + ]; util.selectiveNotDeepExtend(fields, this.options, options); // merge the keyboard options in. diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index b61bdf429..6d50d61d9 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -23,8 +23,13 @@ class SelectionHandler { selectable: true, selectConnectedEdges: true, hoverConnectedEdges: true, - selectionBox: true + selectionBox: { + enabled: true, + nodes: true, + edges: false + } }; + util.extend(this.options, this.defaultOptions); this.body.emitter.on("_dataChanged", () => { @@ -37,10 +42,14 @@ class SelectionHandler { * * @param {Object} [options] */ - setOptions(options) { - if (options !== undefined) { - let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges', 'selectionBox']; + setOptions(options) { + if (options !== undefined) { + let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges']; util.selectiveDeepExtend(fields,this.options, options); + + util.mergeOptions(this.options, options, "selectionBox"); + + console.log(this.options.selectionBox); } } diff --git a/lib/network/options.js b/lib/network/options.js index 44d7a1328..610ec23c1 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -165,9 +165,9 @@ let allOptions = { selectable: { boolean: bool }, selectConnectedEdges: { boolean: bool }, selectionBox: { - enabled: {boolean: bool}, - edges: {boolean: bool }, - nodes: {boolean: bool }, + enabled: { boolean: bool }, + nodes: { boolean: bool }, + edges: { boolean: bool }, __type__: { object, boolean: bool } }, hoverConnectedEdges: { boolean: bool }, @@ -577,7 +577,11 @@ let configureOptions = { navigationButtons: false, selectable: true, selectConnectedEdges: true, - selectionBox: true, + selectionBox: { + enabled: false, + nodes: true, + edges: false + }, hoverConnectedEdges: true, tooltipDelay: [300, 0, 1000, 25], zoomView: true, diff --git a/lib/shared/Configurator.js b/lib/shared/Configurator.js index 0f973430e..d81d9aa68 100644 --- a/lib/shared/Configurator.js +++ b/lib/shared/Configurator.js @@ -1,3 +1,5 @@ +import { debug } from 'util'; + var util = require('../util'); var ColorPicker = require('./ColorPicker').default; @@ -582,7 +584,8 @@ class Configurator { show = true; let item = obj[subObj]; let newPath = util.copyAndExtendArray(path, subObj); - if (typeof filter === 'function') { + + if (typeof filter === 'function') { show = filter(subObj,path); // if needed we must go deeper into the object. @@ -594,7 +597,7 @@ class Configurator { } } } - + if (show !== false) { visibleInSet = true; let value = this._getValue(newPath); @@ -627,6 +630,10 @@ class Configurator { this._makeItem(newPath, label); visibleInSet = this._handleObject(item, newPath) || visibleInSet; } + /*else if (enabledValue === undefined) { + debugger; + this._makeCheckbox(item, this._getValue(newPath), newPath); + }*/ else { this._makeCheckbox(item, enabledValue, newPath); } @@ -699,7 +706,6 @@ class Configurator { */ _constructOptions(value, path, optionsObj = {}) { let pointer = optionsObj; - // when dropdown boxes can be string or boolean, we typecast it into correct types value = value === 'true' ? true : value; value = value === 'false' ? false : value; diff --git a/lib/util.js b/lib/util.js index 2203380db..080acfed8 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1313,7 +1313,7 @@ exports.mergeOptions = function (mergeTarget, options, option, globalOptions = { var globalOption = globalPassed? globalOptions[option]: undefined; var globalEnabled = globalOption? globalOption.enabled: undefined; - + ///////////////////////////////////////// // Main routine ///////////////////////////////////////// From 32fd26638d91ed61e0de1c83840586ce2b198e71 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Fri, 1 Feb 2019 23:38:00 -0500 Subject: [PATCH 04/10] cleaning up box drawing logic --- lib/network/modules/Canvas.js | 5 +- lib/network/modules/InteractionHandler.js | 32 ++++++-- lib/network/modules/SelectionBox.js | 89 ++++++++++++++++++++--- lib/shared/Configurator.js | 12 +-- 4 files changed, 110 insertions(+), 28 deletions(-) diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index b64ed8380..134083eb5 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -254,7 +254,10 @@ class Canvas { this.frame.canvas.addEventListener('mousemove', (event) => {this.body.eventListeners.onMouseMove(event)}); this.frame.canvas.addEventListener('contextmenu', (event) => {this.body.eventListeners.onContext(event)}); - this.frame.canvas.addEventListener('mousedown', (event) => {this.body.eventListeners.onMouseDown(event)}); + //this.frame.canvas.addEventListener('mousedown', (event) => {this.body.eventListeners.onMouseDown(event)}); + + this.frame.canvas.addEventListener('mouseenter', (event) => {this.body.eventListeners.onMouseEnter(event)}); + this.frame.canvas.addEventListener('mouseleave', (event) => {this.body.eventListeners.onMouseLeave(event)}); this.hammerFrame = new Hammer(this.frame); hammerUtil.onRelease(this.hammerFrame, (event) => {this.body.eventListeners.onRelease(event)}); diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 0707bfb8c..1281894be 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -35,9 +35,12 @@ class InteractionHandler { this.body.eventListeners.onRelease = this.onRelease.bind(this); this.body.eventListeners.onContext = this.onContext.bind(this); + this.body.eventListeners.onMouseEnter = this.onMouseEnter.bind(this); + this.body.eventListeners.onMouseLeave = this.onMouseLeave.bind(this); + this.touchTime = 0; this.drag = {}; - this.pinch = {}; + this.pinch = {}; this.popup = undefined; this.popupObj = undefined; this.popupTimer = undefined; @@ -64,6 +67,18 @@ class InteractionHandler { this.bindEventListeners() } + onMouseLeave(event) { + if (this._selectionBoxOption()) { + this.selectionBox.mouseLeave(event); + } + } + + onMouseEnter(event) { + if (this._selectionBoxOption()) { + this.selectionBox.mouseEnter(event); + } + } + _selectionBoxOption() { return this.selectionHandler.options.selectionBox.enabled; //return true; @@ -86,7 +101,10 @@ class InteractionHandler { setOptions(options) { if (options !== undefined) { // extend all but the values in fields - // (there is an overlap between the responsibilites of Network's CanvasRenderer, InteractionHandler and SelectionHandler + // (there is an overlap between the options "interaction" subobject, with options there taken by three seperate components: + // CanvasRenderer, InteractionHandler and SelectionHandler + // it might be good at some point to break the options and program logic up + // so there is a more direct mapping between the structure of the options in options.js and Network's program logic let fields = [ 'hideEdgesOnDrag' /* see CanvasRenderer */, 'hideNodesOnDrag' /* see CanvasRenderer */, @@ -94,7 +112,7 @@ class InteractionHandler { 'multiselect' /* see SelectionHandler */, 'selectable' /* see SelectionHandler */, 'selectConnectedEdges' /* see SelectionHandler */, - 'selectionBox' /* see SelectionHandler*/ + 'selectionBox' /* see SelectionHandler */ ]; util.selectiveNotDeepExtend(fields, this.options, options); @@ -131,7 +149,7 @@ class InteractionHandler { * @param {Event} event The event * @private */ - onTouch(event) { + onTouch(event) { if (new Date().valueOf() - this.touchTime > 50) { if (this._selectionBoxOption()) { if (event.srcEvent.ctrlKey) { @@ -383,12 +401,14 @@ class InteractionHandler { } if (this.selectionBox.isActive()) { + /*console.log(event.srcEvent.offsetX, ",", event.srcEvent.offsetY); + console.log(event.srcEvent); let p = this.canvas.DOMtoCanvas({ x: event.srcEvent.offsetX, y: event.srcEvent.offsetY - }); + });*/ - this.selectionBox.updateBoundingBox(p) + this.selectionBox.updateBoundingBox(event.srcEvent) return; } diff --git a/lib/network/modules/SelectionBox.js b/lib/network/modules/SelectionBox.js index 10bb19a4f..07645e668 100644 --- a/lib/network/modules/SelectionBox.js +++ b/lib/network/modules/SelectionBox.js @@ -6,6 +6,14 @@ class SelectionBox { this.y = 0; this.width = 0; this.height = 0; + + // assist in adjusting width / height correctly when mouse moves out of canvas element + this.domAdjustX = 0; + this.domAdjustY = 0; + this.lastCanvasScreenX = 0; + this.lastCanvasScreenY = 0; + this._consumeMouseEvent = this._defaultMouseEventConsumer; + this.active = false; } @@ -22,9 +30,10 @@ class SelectionBox { // p := {x: number, y: number} // activate(p) { - console.log(this.selectionHandler); this.x = p.x; this.y = p.y; + this.width = 0; + this.height = 0; this.active = true; } @@ -36,13 +45,63 @@ class SelectionBox { this.active = false; } + // + // + // + + _defaultMouseEventConsumer(ev) { + let p = this.selectionHandler.canvas.DOMtoCanvas({ + x: ev.offsetX, + y: ev.offsetY + }); + + let xDir = this.x <= p.x ? 1 : -1; + let yDir = this.y <= p.y ? 1 : -1; + + //this.width = Math.abs(ev.x - this.x) * xDir; + //this.height = Math.abs(ev.y - this.y) * yDir; + let width = Math.abs(p.x - this.x) * xDir; + let height = Math.abs(p.y - this.y) * yDir; + return { + x: width, + y: height + } + } + + _outsideCanvasMouseEventConsumer(ev) { + return { + x: this.width + (ev.movementX * 1 / this.selectionHandler.canvas.body.view.scale), // ev.offsetX + this.domAdjustX, + y: this.height + (ev.movementY * 1 / this.selectionHandler.canvas.body.view.scale) //ev.offsetY + this.domAdjustY + } + } + + mouseLeave(ev) { + if (this.isActive()) { + console.log("MOUSELEAVE " + ev.offsetX + ", " + ev.offsetY); + //this.lastCanvasScreenX = ev.offsetX; + //this.lastCanvasScreenY = ev.offsetY; + //this._consumeMouseEvent = this._findDomAdjustment; + this._consumeMouseEvent = this._outsideCanvasMouseEventConsumer; + } + } + + mouseEnter(ev) { + if (this.isActive()) { + this.lastCanvasScreenX = 0; + this.lastCanvasScreenY = 0; + this.domAdjustX = 0; + this.domAdjustY = 0; + this._consumeMouseEvent = this._defaultMouseEventConsumer; + } + } + // // complete the selectionBox, the user has let go of their mouse button - // select the + // select the nodes and edges within the bounds of the selection box // complete() { let boundingBox = this.getBoundingBox(); - if (this.selectionHandler.options.selectionBox.edges || this.selectionHandler.options.selectionBox === true) { + if (this.selectionHandler.options.selectionBox.edges) { // run the edges first, so that if "this.selectionHandler.options.selectConnectedEdges" is set, // we don't _unset_ edges that _get_ set by our node selection logic let selectedEdgeIds = this.selectionHandler.getAllEdgesWithinBoundingBox(boundingBox); @@ -56,7 +115,7 @@ class SelectionBox { } } } - if (this.selectionHandler.options.selectionBox.nodes || this.selectionHandler.options.selectionBox === true) { + if (this.selectionHandler.options.selectionBox.nodes) { let selectedNodeIds = this.selectionHandler.getAllNodesWithinBoundingBox(boundingBox); for (let nodeId of selectedNodeIds) { let node = this.body.nodes[nodeId]; @@ -71,7 +130,7 @@ class SelectionBox { this.deactivate(); } - // + // // get the current bounding box for the user's selection box // returns {left, top, right, bottom} in canvas model space // @@ -82,7 +141,7 @@ class SelectionBox { right: 0, bottom: 0 } - + if (this.width < 0) { result.left = this.x + this.width; result.right = this.x; @@ -91,7 +150,7 @@ class SelectionBox { result.left = this.x; result.right = this.x + this.width; } - + if (this.height < 0) { result.top = this.y + this.height; result.bottom = this.y; @@ -100,22 +159,28 @@ class SelectionBox { result.top = this.y; result.bottom = this.y + this.height; } - + return result; } // // update the bounding box as per user mouse movement - // p is an object {x, y} in canvas model space + // p is an object {x, y} in canvas model space // new position of mouse is used as new corner of box // (point at which user began the bounding box remains fixed) // - updateBoundingBox(p) { - let xDir = this.x <= p.x ? 1 : -1; - let yDir = this.y <= p.y ? 1 : -1; + updateBoundingBox(/*p*/ ev) { + let {x, y} = this._consumeMouseEvent(ev); + let xDir = this.x <= x ? 1 : -1; + let yDir = this.y <= y ? 1 : -1; + + this.width = x; // Math.abs(x - this.x) * xDir; + this.height = y; // Math.abs(y - this.y) * yDir; + /* this.width = Math.abs(p.x - this.x) * xDir; this.height = Math.abs(p.y - this.y) * yDir; + */ this.body.emitter.emit("_redraw"); } } diff --git a/lib/shared/Configurator.js b/lib/shared/Configurator.js index d81d9aa68..0f973430e 100644 --- a/lib/shared/Configurator.js +++ b/lib/shared/Configurator.js @@ -1,5 +1,3 @@ -import { debug } from 'util'; - var util = require('../util'); var ColorPicker = require('./ColorPicker').default; @@ -584,8 +582,7 @@ class Configurator { show = true; let item = obj[subObj]; let newPath = util.copyAndExtendArray(path, subObj); - - if (typeof filter === 'function') { + if (typeof filter === 'function') { show = filter(subObj,path); // if needed we must go deeper into the object. @@ -597,7 +594,7 @@ class Configurator { } } } - + if (show !== false) { visibleInSet = true; let value = this._getValue(newPath); @@ -630,10 +627,6 @@ class Configurator { this._makeItem(newPath, label); visibleInSet = this._handleObject(item, newPath) || visibleInSet; } - /*else if (enabledValue === undefined) { - debugger; - this._makeCheckbox(item, this._getValue(newPath), newPath); - }*/ else { this._makeCheckbox(item, enabledValue, newPath); } @@ -706,6 +699,7 @@ class Configurator { */ _constructOptions(value, path, optionsObj = {}) { let pointer = optionsObj; + // when dropdown boxes can be string or boolean, we typecast it into correct types value = value === 'true' ? true : value; value = value === 'false' ? false : value; From ca83e3fea59951bdc7b3fd5527d2fb013e1e55e2 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Sat, 2 Feb 2019 18:56:41 -0500 Subject: [PATCH 05/10] Fully working, roll it out to world for debug and feedback --- docs/network/interaction.html | 19 ++ examples/network/other/selectionBox.html | 97 ++++++++++ lib/network/modules/Canvas.js | 4 - lib/network/modules/CanvasRenderer.js | 12 +- lib/network/modules/InteractionHandler.js | 19 +- lib/network/modules/SelectionBox.js | 211 +++++++++++++--------- lib/network/modules/SelectionHandler.js | 30 +-- lib/network/modules/components/Edge.js | 44 +---- lib/network/options.js | 10 +- 9 files changed, 280 insertions(+), 166 deletions(-) create mode 100644 examples/network/other/selectionBox.html diff --git a/docs/network/interaction.html b/docs/network/interaction.html index ad3deb159..fe7f768e4 100644 --- a/docs/network/interaction.html +++ b/docs/network/interaction.html @@ -66,6 +66,14 @@

Options

bindToWindow: true }, multiselect: false, + selectionBox: { + enabled: false, + nodes: true, + edges: false, + strokeStyle: "rgb(0,0,0)", + fillStyle: "rgba(0,0,0,.0625)", + edgeAccuracy: 25 + } navigationButtons: false, selectable: true, selectConnectedEdges: true, @@ -98,13 +106,24 @@

Options

hideNodesOnDrag Boolean false When true, the nodes are not drawn when dragging the view. This can greatly speed up responsiveness on dragging, improving user experience. hover Boolean false When true, the nodes use their hover colors when the mouse moves over them. hoverConnectedEdges Boolean true When true, on hovering over a node, it's connecting edges are highlighted. + keyboard Object or Boolean Object When true, the keyboard shortcuts are enabled with the default settings. For further customization, you can supply an object. keyboard.enabled Boolean false Toggle the usage of the keyboard shortcuts. If this option is not defined, it is set to true if any of the properties in this object are defined. keyboard.speed.x Number 1 The speed at which the view moves in the x direction on pressing a key or pressing a navigation button. keyboard.speed.y Number 1 The speed at which the view moves in the y direction on pressing a key or pressing a navigation button. keyboard.speed.zoom Number 0.02 The speed at which the view zooms in or out pressing a key or pressing a navigation button. keyboard.bindToWindow Boolean true When binding the keyboard shortcuts to the window, they will work regardless of which DOM object has the focus. If you have multiple networks on your page, you could set this to false, making sure the keyboard shortcuts only work on the network that has the focus. + multiselect Boolean false When true, a longheld click (or touch) as well as a control-click will add to the selection. + + selectionBox Object or Boolean Object When true, the selectionBox is enabled with the default settings. For further customization, you can supply an object. + selectionBox.enabledBoolean false Toggle the usage of the selectionBox shortcuts + selectionBox.nodesBoolean true Whether the selection box will select nodes when the user releases the box. + selectionBox.edgesBoolean false Whether the selection box will select edges when the user releases the box. + selectionBox.strokeStyleString rgb(0,0,0)The style of the selection box's border. Any valid color,gradient or pattern will work (see MDN: CanvasRenderingContext2D.strokeStyle), but there is no error checking for this value! + selectionBox.fillStyleString rgba(0,0,0,.0625)The style of the selection box's fill. Any valid color,gradient or pattern will work (see MDN: CanvasRenderingContext2D.fillStyle), but there is no error checking for this value! + selectionBox.edgeAccuracyNumber 25 "Accuracy" of the edge detection. Edges in Vis.js are Bezier curves, so we plot points along an edge and check if a point is within our selection box. The number of points per edge used during this process is specified with this value. + navigationButtons Boolean false When true, navigation buttons are drawn on the network canvas. These are HTML buttons and can be completely customized using CSS. selectable BooleantrueWhen true, the nodes and edges can be selected by the user. selectConnectedEdges BooleantrueWhen true, on selecting a node, its connecting edges are highlighted. diff --git a/examples/network/other/selectionBox.html b/examples/network/other/selectionBox.html new file mode 100644 index 000000000..f84d7bbba --- /dev/null +++ b/examples/network/other/selectionBox.html @@ -0,0 +1,97 @@ + + + + Network | selectionBox option + + + + + + + + + + + + + + +

+ This example shows how the selection box works. Press and hold the control key and click with your mouse on an area within the Network. + Drag and release to select nodes and edges within the bounds of the resulting box. Here, you can enable / disable the options (or the entire feature) in the configurator. + To modify the options programmatically simply call Ne +

+

+ Options available: +

    +
  • "nodes": Select nodes within the box's bounds
  • +
  • "edges": Select edges within the box's bounds
  • +
  • "strokeStyle": An [RGB|RGBA] string to set the color of the box's border
  • +
  • "fillStyle": An RGBA string to set the color of the box's fill (you probably want this at a low opacity)
  • +
  • "edgeAccuracy": The number of points generated on each edge that are used to check if the edge is contaned within the box + The default is 25, which means for every edge, we calculate at most 25 evenly distributed points across each line. +
  • +
+

+
+
+ +

+ + diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index 134083eb5..753e56cb9 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -254,10 +254,6 @@ class Canvas { this.frame.canvas.addEventListener('mousemove', (event) => {this.body.eventListeners.onMouseMove(event)}); this.frame.canvas.addEventListener('contextmenu', (event) => {this.body.eventListeners.onContext(event)}); - //this.frame.canvas.addEventListener('mousedown', (event) => {this.body.eventListeners.onMouseDown(event)}); - - this.frame.canvas.addEventListener('mouseenter', (event) => {this.body.eventListeners.onMouseEnter(event)}); - this.frame.canvas.addEventListener('mouseleave', (event) => {this.body.eventListeners.onMouseLeave(event)}); this.hammerFrame = new Hammer(this.frame); hammerUtil.onRelease(this.hammerFrame, (event) => {this.body.eventListeners.onRelease(event)}); diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index a29b32556..8c82a7212 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -138,7 +138,7 @@ class CanvasRenderer { * @returns {function|undefined} * @private */ - _requestNextFrame(callback, delay) { + _requestNextFrame(callback, delay) { // During unit testing, it happens that the mock window object is reset while // the next frame is still pending. Then, either 'window' is not present, or // 'requestAnimationFrame()' is not present because it is not defined on the @@ -274,7 +274,8 @@ class CanvasRenderer { this._drawNodes(ctx, hidden); } - // if it should draw selection box: + + // if it should draw selection box if (this.selectionBox.isActive()) { this._drawSelectionBox(ctx); } @@ -295,11 +296,14 @@ class CanvasRenderer { _drawSelectionBox(ctx) { ctx.save(); { + ctx.beginPath(); + ctx.strokeStyle = this.selectionBox.options.strokeStyle; + ctx.fillStyle = this.selectionBox.options.fillStyle; + ctx.rect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height); ctx.stroke(); - - ctx.fillStyle = "rgba(0, 0, 0, 0.0625)"; ctx.fillRect(this.selectionBox.x + 1, this.selectionBox.y + 1, this.selectionBox.width - 2, this.selectionBox.height - 2); + ctx.closePath(); } ctx.restore(); } diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index 1281894be..bcf1c88aa 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -35,9 +35,6 @@ class InteractionHandler { this.body.eventListeners.onRelease = this.onRelease.bind(this); this.body.eventListeners.onContext = this.onContext.bind(this); - this.body.eventListeners.onMouseEnter = this.onMouseEnter.bind(this); - this.body.eventListeners.onMouseLeave = this.onMouseLeave.bind(this); - this.touchTime = 0; this.drag = {}; this.pinch = {}; @@ -67,18 +64,6 @@ class InteractionHandler { this.bindEventListeners() } - onMouseLeave(event) { - if (this._selectionBoxOption()) { - this.selectionBox.mouseLeave(event); - } - } - - onMouseEnter(event) { - if (this._selectionBoxOption()) { - this.selectionBox.mouseEnter(event); - } - } - _selectionBoxOption() { return this.selectionHandler.options.selectionBox.enabled; //return true; @@ -157,7 +142,7 @@ class InteractionHandler { x: event.srcEvent.offsetX, y: event.srcEvent.offsetY }); - this.selectionBox.activate(p); + this.selectionBox.activate(event.srcEvent); } } @@ -223,7 +208,7 @@ class InteractionHandler { onRelease(event) { if (new Date().valueOf() - this.touchTime > 10) { if (this.selectionBox.isActive()) { - this.selectionBox.complete(); + this.selectionBox.release(); } let pointer = this.getPointer(event.center); this.selectionHandler._generateClickEvent('release', event, pointer); diff --git a/lib/network/modules/SelectionBox.js b/lib/network/modules/SelectionBox.js index 07645e668..55b05e981 100644 --- a/lib/network/modules/SelectionBox.js +++ b/lib/network/modules/SelectionBox.js @@ -2,18 +2,13 @@ class SelectionBox { constructor(body, selectionHandler) { this.body = body; this.selectionHandler = selectionHandler; + this.x = 0; this.y = 0; this.width = 0; this.height = 0; - // assist in adjusting width / height correctly when mouse moves out of canvas element - this.domAdjustX = 0; - this.domAdjustY = 0; - this.lastCanvasScreenX = 0; - this.lastCanvasScreenY = 0; - this._consumeMouseEvent = this._defaultMouseEventConsumer; - + this.options = this.selectionHandler.options.selectionBox; this.active = false; } @@ -24,12 +19,16 @@ class SelectionBox { return this.active; } - // - // activate the selectionBox - // pass in the origin point in canvas space - // p := {x: number, y: number} - // - activate(p) { + /** + * activate the selectionBox, user has pressed ctrl + mousebutton + * @param {MouseEvent} ev + */ + activate(ev) { + let p = this.selectionHandler.canvas.DOMtoCanvas({ + x: ev.offsetX, + y: ev.offsetY + }); + this.x = p.x; this.y = p.y; this.width = 0; @@ -45,61 +44,12 @@ class SelectionBox { this.active = false; } - // - // - // - - _defaultMouseEventConsumer(ev) { - let p = this.selectionHandler.canvas.DOMtoCanvas({ - x: ev.offsetX, - y: ev.offsetY - }); - - let xDir = this.x <= p.x ? 1 : -1; - let yDir = this.y <= p.y ? 1 : -1; - - //this.width = Math.abs(ev.x - this.x) * xDir; - //this.height = Math.abs(ev.y - this.y) * yDir; - let width = Math.abs(p.x - this.x) * xDir; - let height = Math.abs(p.y - this.y) * yDir; - return { - x: width, - y: height - } - } - - _outsideCanvasMouseEventConsumer(ev) { - return { - x: this.width + (ev.movementX * 1 / this.selectionHandler.canvas.body.view.scale), // ev.offsetX + this.domAdjustX, - y: this.height + (ev.movementY * 1 / this.selectionHandler.canvas.body.view.scale) //ev.offsetY + this.domAdjustY - } - } - - mouseLeave(ev) { - if (this.isActive()) { - console.log("MOUSELEAVE " + ev.offsetX + ", " + ev.offsetY); - //this.lastCanvasScreenX = ev.offsetX; - //this.lastCanvasScreenY = ev.offsetY; - //this._consumeMouseEvent = this._findDomAdjustment; - this._consumeMouseEvent = this._outsideCanvasMouseEventConsumer; - } - } - - mouseEnter(ev) { - if (this.isActive()) { - this.lastCanvasScreenX = 0; - this.lastCanvasScreenY = 0; - this.domAdjustX = 0; - this.domAdjustY = 0; - this._consumeMouseEvent = this._defaultMouseEventConsumer; - } - } // - // complete the selectionBox, the user has let go of their mouse button + // the user has let go of their mouse button, release the selectionBox // select the nodes and edges within the bounds of the selection box // - complete() { + release() { let boundingBox = this.getBoundingBox(); if (this.selectionHandler.options.selectionBox.edges) { // run the edges first, so that if "this.selectionHandler.options.selectConnectedEdges" is set, @@ -130,6 +80,100 @@ class SelectionBox { this.deactivate(); } + /** + * Calculate new box corner {x,y} based on mouse input + * returning values in canvas space + * this is easy when the user remains in the canvas + * but needs a bit of extra logic to handle a mouse movement that extends outside of the canvas + * NOTE: the order of the per-axis predicates is important here! + * + * @param {MouseEvent} ev + * + */ + _consumeMouseEvent(ev) { + let frameRect = this.selectionHandler.canvas.frame.getBoundingClientRect(); + let x, y; + + // + // X-Axis + // + + // mouse to the left of viewport + if (ev.clientX < 0) { + // is top of frame also above viewport? + if (frameRect.left < 0) { + x = (-frameRect.left) + 1; + } + else { + x = 1; + } + } + // mouse to the left of frame + else if (ev.clientX < frameRect.left) { + x = 1; + } + // mouse to the right of viewport + else if (ev.clientX > window.innerWidth) { + // is right of frame also beyond viewport? + if (frameRect.right > window.innerWidth) { + x = window.innerWidth - frameRect.left - 1; + } + else { + x = frameRect.right - frameRect.left - 1; + } + } + // mouse to the right of frame + else if (ev.clientX > frameRect.right) { + x = frameRect.right - frameRect.left - 1; + } + // mouse horizontally within frame + else { + x = ev.clientX - frameRect.x; + } + + // + // Y-Axis + // + + // mouse above viewport + if (ev.clientY < 0) { + // is top of frame also above viewport? + if (frameRect.top < 0) { + y = (-frameRect.top) + 1; + } + else { + y = 1; + } + } + // mouse above frame + else if (ev.clientY < frameRect.top) { + y = 1; + } + // mouse below viewport + else if (ev.clientY > window.innerHeight) { + // is bottom of frame also below viewport? + if (frameRect.bottom > window.innerHeight) { + y = window.innerHeight - frameRect.top - 1; + } + else { + y = frameRect.bottom - frameRect.top - 1; + } + } + // mouse below frame + else if (ev.clientY > frameRect.bottom) { + y = frameRect.bottom - frameRect.top - 1; + } + // mouse vertically within frame + else { + y = ev.clientY - frameRect.y; + } + + return this.selectionHandler.canvas.DOMtoCanvas({ + x: x, + y: y + }); + } + // // get the current bounding box for the user's selection box // returns {left, top, right, bottom} in canvas model space @@ -161,28 +205,25 @@ class SelectionBox { } return result; - } - - // - // update the bounding box as per user mouse movement - // p is an object {x, y} in canvas model space - // new position of mouse is used as new corner of box - // (point at which user began the bounding box remains fixed) - // - updateBoundingBox(/*p*/ ev) { - let {x, y} = this._consumeMouseEvent(ev); - let xDir = this.x <= x ? 1 : -1; - let yDir = this.y <= y ? 1 : -1; - - this.width = x; // Math.abs(x - this.x) * xDir; - this.height = y; // Math.abs(y - this.y) * yDir; - - /* - this.width = Math.abs(p.x - this.x) * xDir; - this.height = Math.abs(p.y - this.y) * yDir; - */ + } + + /** + * update the bounding box as per user mouse movement + * new position of mouse is used as new corner of box + * (point at which user began the bounding box remains fixed) + * + * @param {MouseEvent} ev + */ + updateBoundingBox(ev) { + let {x: newX, y: newY} = this._consumeMouseEvent(ev); + let xDir = this.x <= newX ? 1 : -1; + let yDir = this.y <= newY ? 1 : -1; + + this.width = Math.abs(newX - this.x) * xDir; + this.height = Math.abs(newY - this.y) * yDir; + this.body.emitter.emit("_redraw"); - } + } } export default SelectionBox; \ No newline at end of file diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index 6d50d61d9..bd1f29adb 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -24,9 +24,12 @@ class SelectionHandler { selectConnectedEdges: true, hoverConnectedEdges: true, selectionBox: { - enabled: true, + enabled: false, nodes: true, - edges: false + edges: false, + strokeStyle: "rgb(0,0,0)", + fillStyle: "rgba(0,0,0,.0625)", + edgeAccuracy: 25 } }; @@ -42,14 +45,12 @@ class SelectionHandler { * * @param {Object} [options] */ - setOptions(options) { - if (options !== undefined) { + setOptions(options) { + if (options !== undefined) { let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges']; util.selectiveDeepExtend(fields,this.options, options); util.mergeOptions(this.options, options, "selectionBox"); - - console.log(this.options.selectionBox); } } @@ -216,7 +217,7 @@ class SelectionHandler { return this._getAllNodesOverlappingWith(object); } - + /** @@ -273,7 +274,7 @@ class SelectionHandler { _getEdgesOverlappingWith(object, overlappingEdges) { let edges = this.body.edges; for (let i = 0; i < this.body.edgeIndices.length; i++) { - let edgeId = this.body.edgeIndices[i]; + let edgeId = this.body.edgeIndices[i]; if (edges[edgeId].isOverlappingWith(object)) { overlappingEdges.push(edgeId); } @@ -293,11 +294,16 @@ class SelectionHandler { return overlappingEdges; } - getAllEdgesWithinBoundingBox(object) { + /** + * get edge ids of all edges within a bounding box (bounding box in canvas model space) + * @param {{left: number, top: number, right: number, bottom:number}} boundingBox + * @return edgeId[] + */ + getAllEdgesWithinBoundingBox(boundingBox) { let result = []; for (let i = 0; i < this.body.edgeIndices.length; i++) { - let edgeId = this.body.edgeIndices[i]; - if (this.body.edges[edgeId].SB_checkBoundingBox(object, 25)) { + let edgeId = this.body.edgeIndices[i]; + if (this.body.edges[edgeId].SB_checkBoundingBox(boundingBox, this.options.selectionBox.edgeAccuracy)) { result.push(edgeId); } } @@ -855,7 +861,7 @@ class SelectionHandler { * @param {point} pointer mouse position in screen coordinates * @returns {Array.} * @private - */ + */ getClickedItems(pointer) { let point = this.canvas.DOMtoCanvas(pointer); var items = []; diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index f00cf834a..043583cc0 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -52,7 +52,7 @@ class Edge { this.setOptions(options); } - SB_checkBoundingBox(boundingBox, points = 5) { + SB_checkBoundingBox(boundingBox, points) { let step = 1 / points; let percentage = 0; for (let i = 0; i < points; i++) { @@ -694,47 +694,7 @@ class Edge { } } - SB_existsWithinBoundingBox(boundingBox) { - let thisBox = { - left: 0, - top: 0, - right: 0, - bottom: 0 - }; - - if (this.from.x < this.to.x) { - thisBox.left = this.from.x; - thisBox.right = this.to.x; - } - else { - thisBox.left = this.to.x; - thisBox.right = this.from.x; - } - - if (this.from.y < this.to.y) { - thisBox.top = this.from.y; - thisBox.bottom = this.to.y; - } - else { - thisBox.top = this.to.y; - thisBox.bottom = this.from.y; - } - - console.log(boundingBox); - console.log(thisBox); - console.log("===="); - if (thisBox.left >= boundingBox.left && - thisBox.right <= boundingBox.right && - thisBox.top >= boundingBox.top && - thisBox.bottom <= boundingBox.bottom) { - return true; - } - else { - return false; - } - } - - /** + /** * Determine the rotation point, if any. * * @param {CanvasRenderingContext2D} [ctx] if passed, do a recalculation of the label size diff --git a/lib/network/options.js b/lib/network/options.js index 610ec23c1..5740d5a62 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -167,7 +167,10 @@ let allOptions = { selectionBox: { enabled: { boolean: bool }, nodes: { boolean: bool }, - edges: { boolean: bool }, + edges: { boolean: bool }, + strokeStyle: { string }, + fillStyle: { string }, + edgeAccuracy: { number }, __type__: { object, boolean: bool } }, hoverConnectedEdges: { boolean: bool }, @@ -580,7 +583,10 @@ let configureOptions = { selectionBox: { enabled: false, nodes: true, - edges: false + edges: false, + strokeStyle: "rgb(0,0,0)", + fillStyle: "rgba(0,0,0,.0625)", + edgeAccuracy: [25, 1, 100, 1] }, hoverConnectedEdges: true, tooltipDelay: [300, 0, 1000, 25], From dd76ffc041c0e6db78a918eca35d3528abe8aa91 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Sat, 2 Feb 2019 21:32:48 -0500 Subject: [PATCH 06/10] fix broken tests --- lib/network/modules/CanvasRenderer.js | 6 ++++ lib/network/modules/InteractionHandler.js | 11 +++---- lib/network/modules/SelectionBox.js | 38 +++++++++++++++++------ lib/network/modules/SelectionHandler.js | 14 +++++---- lib/network/modules/components/Edge.js | 8 +++++ 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index 8c82a7212..bed0f79b1 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -50,6 +50,7 @@ class CanvasRenderer { /** * @param {Object} body * @param {Canvas} canvas + * @param {SelectionBox} selectionBox */ constructor(body, canvas, selectionBox) { _initRequestAnimationFrame(); @@ -293,6 +294,11 @@ class CanvasRenderer { } } + /** + * Draws the selectionBox + * @param {CanvasRenderingContext2D} ctx + * @private + */ _drawSelectionBox(ctx) { ctx.save(); { diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index bcf1c88aa..ceca383d4 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -1,8 +1,6 @@ let util = require('../../util'); var NavigationHandler = require('./components/NavigationHandler').default; var Popup = require('./../../shared/Popup').default; -var SelectionBox = require('./SelectionBox').default; - /** * Handler for interactions @@ -64,9 +62,12 @@ class InteractionHandler { this.bindEventListeners() } + /** + * Helper function to check if the selectionBox option is enabled + * @private + */ _selectionBoxOption() { return this.selectionHandler.options.selectionBox.enabled; - //return true; } /** @@ -138,10 +139,6 @@ class InteractionHandler { if (new Date().valueOf() - this.touchTime > 50) { if (this._selectionBoxOption()) { if (event.srcEvent.ctrlKey) { - let p = this.canvas.DOMtoCanvas({ - x: event.srcEvent.offsetX, - y: event.srcEvent.offsetY - }); this.selectionBox.activate(event.srcEvent); } } diff --git a/lib/network/modules/SelectionBox.js b/lib/network/modules/SelectionBox.js index 55b05e981..c7d35f238 100644 --- a/lib/network/modules/SelectionBox.js +++ b/lib/network/modules/SelectionBox.js @@ -1,10 +1,20 @@ +/** + * SelectionBox + */ class SelectionBox { + /** + * + * @param {Network.body} body + * @param {Network.SelectionHandler} selectionHandler + */ constructor(body, selectionHandler) { this.body = body; this.selectionHandler = selectionHandler; + // corner from initial ctrl+click, canvas model space this.x = 0; this.y = 0; + // width and height recalculated on mousemove events, canvas model space this.width = 0; this.height = 0; @@ -12,9 +22,9 @@ class SelectionBox { this.active = false; } - // - // check to see if the user is currently drawing a selection box - // + /** + * check to see if user is currently drawing a selection box + */ isActive() { return this.active; } @@ -36,7 +46,11 @@ class SelectionBox { this.active = true; } - deactivate() { + /** + * reset the selection box state + * @private + */ + _deactivate() { this.x = 0; this.y = 0; this.width = 0; @@ -45,10 +59,10 @@ class SelectionBox { } - // - // the user has let go of their mouse button, release the selectionBox - // select the nodes and edges within the bounds of the selection box - // + /** + * called when user release mouse button, completing their selection box + * selects the nodes and edges within the selection box + */ release() { let boundingBox = this.getBoundingBox(); if (this.selectionHandler.options.selectionBox.edges) { @@ -77,7 +91,7 @@ class SelectionBox { } } } - this.deactivate(); + this._deactivate(); } /** @@ -88,7 +102,7 @@ class SelectionBox { * NOTE: the order of the per-axis predicates is important here! * * @param {MouseEvent} ev - * + * @returns {{x: number, y: number}} */ _consumeMouseEvent(ev) { let frameRect = this.selectionHandler.canvas.frame.getBoundingClientRect(); @@ -178,6 +192,10 @@ class SelectionBox { // get the current bounding box for the user's selection box // returns {left, top, right, bottom} in canvas model space // + /** + * get the current bounding box for the user's selection box + * @return {{left: number, top: number, right: number, bottom: number}} + */ getBoundingBox() { let result = { left: 0, diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index bd1f29adb..f58b9d9ab 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -213,13 +213,15 @@ class SelectionHandler { return overlappingNodes; } - getAllNodesWithinBoundingBox(object) { - return this._getAllNodesOverlappingWith(object); + /** + * + * @param {{left: number, top: number, right: number, bottom: number}} boundingBox + * @return {number[]} An array with node id's + */ + getAllNodesWithinBoundingBox(boundingBox) { + return this._getAllNodesOverlappingWith(boundingBox); } - - - /** * Return a position object in canvasspace from a single point in screenspace * @@ -297,7 +299,7 @@ class SelectionHandler { /** * get edge ids of all edges within a bounding box (bounding box in canvas model space) * @param {{left: number, top: number, right: number, bottom:number}} boundingBox - * @return edgeId[] + * @return {any[]} Array with edge id's */ getAllEdgesWithinBoundingBox(boundingBox) { let result = []; diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index 043583cc0..938e56062 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -52,6 +52,14 @@ class Edge { this.setOptions(options); } + /** + * Initially written to facilitate the selectionBox interaction option. + * checks N evenly spaced points across this edge bezier to see if any of them lay within the bounding box + * + * @param {{left: number, top: number, right: number, bottom: number}} boundingBox + * @param {number} points + * @return {boolean} whether the edge exists at least minimally within a given bounding box + */ SB_checkBoundingBox(boundingBox, points) { let step = 1 / points; let percentage = 0; From 4b272e5852cd870399aebf02ed139f23252c82bf Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Sat, 2 Feb 2019 21:50:16 -0500 Subject: [PATCH 07/10] fixing broken tests --- lib/network/modules/InteractionHandler.js | 4 ++-- lib/network/modules/SelectionBox.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js index ceca383d4..447dcc7cf 100644 --- a/lib/network/modules/InteractionHandler.js +++ b/lib/network/modules/InteractionHandler.js @@ -63,7 +63,7 @@ class InteractionHandler { } /** - * Helper function to check if the selectionBox option is enabled + * @returns {boolean} if the selectionBox option is enabled * @private */ _selectionBoxOption() { @@ -119,7 +119,7 @@ class InteractionHandler { /** * Get the pointer location from a touch location * @param {{x: number, y: number}} touch - * @return {{x: number, y: number}} pointer + * @returns {{x: number, y: number}} pointer * @private */ getPointer(touch) { diff --git a/lib/network/modules/SelectionBox.js b/lib/network/modules/SelectionBox.js index c7d35f238..85e1560d4 100644 --- a/lib/network/modules/SelectionBox.js +++ b/lib/network/modules/SelectionBox.js @@ -23,7 +23,7 @@ class SelectionBox { } /** - * check to see if user is currently drawing a selection box + * @returns {boolean} if user is currently drawing a selection box */ isActive() { return this.active; From f2bec3e6a28bd0e8e48dbbfaf90c3f886dfeaa5e Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Sat, 2 Feb 2019 22:24:56 -0500 Subject: [PATCH 08/10] fix failing test from ItemSet.js --- lib/timeline/component/ItemSet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 485c5fe27..5e615b7af 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -541,6 +541,7 @@ ItemSet.prototype.show = function() { /** * Activates the popup timer to show the given popup after a fixed time. + * @param {any} popup */ ItemSet.prototype.setPopupTimer = function (popup) { this.clearPopupTimer(); From e0e7acf1b2450508632f41216aa0c00a59797d10 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Wed, 6 Feb 2019 22:05:33 -0500 Subject: [PATCH 09/10] added lineWidth option --- docs/network/interaction.html | 1 + examples/network/other/selectionBox.html | 1 + lib/network/modules/CanvasRenderer.js | 18 ++++-- lib/network/modules/SelectionBox.js | 72 ++++++++++++------------ lib/network/modules/SelectionHandler.js | 14 +++++ lib/network/options.js | 2 + 6 files changed, 65 insertions(+), 43 deletions(-) diff --git a/docs/network/interaction.html b/docs/network/interaction.html index fe7f768e4..0c8ba638a 100644 --- a/docs/network/interaction.html +++ b/docs/network/interaction.html @@ -122,6 +122,7 @@

Options

selectionBox.edgesBoolean false Whether the selection box will select edges when the user releases the box. selectionBox.strokeStyleString rgb(0,0,0)The style of the selection box's border. Any valid color,gradient or pattern will work (see MDN: CanvasRenderingContext2D.strokeStyle), but there is no error checking for this value! selectionBox.fillStyleString rgba(0,0,0,.0625)The style of the selection box's fill. Any valid color,gradient or pattern will work (see MDN: CanvasRenderingContext2D.fillStyle), but there is no error checking for this value! + selectionBox.lineWidthNumber (integer) >= 0 2Width of the border line for the selection box, roughly in pixels (some aliasing occurs due to canvas scaling). When setting this value, Vis.js rounds it down to the nearest integer (i.e., 3.2 becomes 3), and forces it to 0 if assigned a negative value. A zero value results in no border being drawn. selectionBox.edgeAccuracyNumber 25 "Accuracy" of the edge detection. Edges in Vis.js are Bezier curves, so we plot points along an edge and check if a point is within our selection box. The number of points per edge used during this process is specified with this value. navigationButtons Boolean false When true, navigation buttons are drawn on the network canvas. These are HTML buttons and can be completely customized using CSS. diff --git a/examples/network/other/selectionBox.html b/examples/network/other/selectionBox.html index f84d7bbba..e44363546 100644 --- a/examples/network/other/selectionBox.html +++ b/examples/network/other/selectionBox.html @@ -84,6 +84,7 @@
  • "edges": Select edges within the box's bounds
  • "strokeStyle": An [RGB|RGBA] string to set the color of the box's border
  • "fillStyle": An RGBA string to set the color of the box's fill (you probably want this at a low opacity)
  • +
  • "lineWidth": Width of line, roughly in pixels (some aliasing occurs due to canvas scaling).
  • "edgeAccuracy": The number of points generated on each edge that are used to check if the edge is contaned within the box The default is 25, which means for every edge, we calculate at most 25 evenly distributed points across each line.
  • diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index bed0f79b1..75e6d1bd7 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -301,16 +301,22 @@ class CanvasRenderer { */ _drawSelectionBox(ctx) { ctx.save(); + ctx.beginPath(); { - ctx.beginPath(); + ctx.lineWidth = this.selectionBox.options.lineWidth / this.body.view.scale; ctx.strokeStyle = this.selectionBox.options.strokeStyle; ctx.fillStyle = this.selectionBox.options.fillStyle; - - ctx.rect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height); - ctx.stroke(); - ctx.fillRect(this.selectionBox.x + 1, this.selectionBox.y + 1, this.selectionBox.width - 2, this.selectionBox.height - 2); - ctx.closePath(); + + // draw the fill rect first, then the border ON TOP of it + ctx.fillRect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height); + + // CanvasRenderingContext2d.lineWidth ignores 0 values, so if the user has set the width to 0, just don't draw the border + if (this.selectionBox.options.lineWidth >= 1) { + ctx.rect(this.selectionBox.x, this.selectionBox.y, this.selectionBox.width, this.selectionBox.height); + ctx.stroke(); + } } + ctx.closePath(); ctx.restore(); } diff --git a/lib/network/modules/SelectionBox.js b/lib/network/modules/SelectionBox.js index 85e1560d4..aeeabba5c 100644 --- a/lib/network/modules/SelectionBox.js +++ b/lib/network/modules/SelectionBox.js @@ -95,11 +95,13 @@ class SelectionBox { } /** - * Calculate new box corner {x,y} based on mouse input + * Calculate new box corner {x,y} based on mouse input, taking into account the width of the line in screen pixels * returning values in canvas space * this is easy when the user remains in the canvas * but needs a bit of extra logic to handle a mouse movement that extends outside of the canvas - * NOTE: the order of the per-axis predicates is important here! + * NOTE' : adjusting for the user's line width may leave out some nodes/edges between the canvas border and (linewWidth / 2) pixels from the border! + * if this is an issue it can surely be fixed + * NOTE'': the order of the per-axis predicates is important here! * * @param {MouseEvent} ev * @returns {{x: number, y: number}} @@ -112,35 +114,35 @@ class SelectionBox { // X-Axis // - // mouse to the left of viewport - if (ev.clientX < 0) { - // is top of frame also above viewport? + // mouse to the left of browser viewport + if (ev.clientX - (this.options.lineWidth / 2) < 0) { + // is left of frame also to left of viewport? if (frameRect.left < 0) { - x = (-frameRect.left) + 1; + x = (-frameRect.left) + Math.ceil(this.options.lineWidth / 2); } else { - x = 1; + x = Math.ceil(this.options.lineWidth / 2); } } - // mouse to the left of frame - else if (ev.clientX < frameRect.left) { - x = 1; + // mouse to the left of canvas + else if (ev.clientX - (this.options.lineWidth / 2) < frameRect.left) { + x = Math.ceil(this.options.lineWidth / 2); } - // mouse to the right of viewport - else if (ev.clientX > window.innerWidth) { + // mouse to the right of browser viewport + else if (ev.clientX + (this.options.lineWidth / 2) > window.innerWidth) { // is right of frame also beyond viewport? if (frameRect.right > window.innerWidth) { - x = window.innerWidth - frameRect.left - 1; + x = window.innerWidth - frameRect.left - Math.ceil(this.options.lineWidth / 2); } else { - x = frameRect.right - frameRect.left - 1; + x = frameRect.right - frameRect.left - Math.ceil(this.options.lineWidth / 2); } } - // mouse to the right of frame - else if (ev.clientX > frameRect.right) { - x = frameRect.right - frameRect.left - 1; + // mouse to the right of canvas + else if (ev.clientX + (this.options.lineWidth / 2) > frameRect.right) { + x = frameRect.right - frameRect.left - Math.ceil(this.options.lineWidth / 2); } - // mouse horizontally within frame + // mouse horizontally within canvas else { x = ev.clientX - frameRect.x; } @@ -149,35 +151,35 @@ class SelectionBox { // Y-Axis // - // mouse above viewport - if (ev.clientY < 0) { + // mouse above browser viewport + if (ev.clientY - (this.options.lineWidth / 2) < 0) { // is top of frame also above viewport? if (frameRect.top < 0) { - y = (-frameRect.top) + 1; + y = (-frameRect.top) + Math.ceil(this.options.lineWidth / 2); } else { - y = 1; + y = Math.ceil(this.options.lineWidth / 2); } } - // mouse above frame - else if (ev.clientY < frameRect.top) { - y = 1; + // mouse above canvas + else if (ev.clientY - (this.options.lineWidth / 2) < frameRect.top) { + y = Math.ceil(this.options.lineWidth / 2); } - // mouse below viewport - else if (ev.clientY > window.innerHeight) { + // mouse below browser viewport + else if (ev.clientY + (this.options.lineWidth / 2) > window.innerHeight) { // is bottom of frame also below viewport? if (frameRect.bottom > window.innerHeight) { - y = window.innerHeight - frameRect.top - 1; + y = window.innerHeight - frameRect.top - Math.ceil(this.options.lineWidth / 2); } else { - y = frameRect.bottom - frameRect.top - 1; + y = frameRect.bottom - frameRect.top - Math.ceil(this.options.lineWidth / 2); } } - // mouse below frame - else if (ev.clientY > frameRect.bottom) { - y = frameRect.bottom - frameRect.top - 1; + // mouse below canvas + else if (ev.clientY + (this.options.lineWidth / 2) > frameRect.bottom) { + y = frameRect.bottom - frameRect.top - Math.ceil(this.options.lineWidth / 2); } - // mouse vertically within frame + // mouse vertically within canvas else { y = ev.clientY - frameRect.y; } @@ -188,10 +190,6 @@ class SelectionBox { }); } - // - // get the current bounding box for the user's selection box - // returns {left, top, right, bottom} in canvas model space - // /** * get the current bounding box for the user's selection box * @return {{left: number, top: number, right: number, bottom: number}} diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index f58b9d9ab..e9703ac10 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -29,6 +29,7 @@ class SelectionHandler { edges: false, strokeStyle: "rgb(0,0,0)", fillStyle: "rgba(0,0,0,.0625)", + lineWidth: 2, edgeAccuracy: 25 } }; @@ -50,6 +51,19 @@ class SelectionHandler { let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges']; util.selectiveDeepExtend(fields,this.options, options); + // + // if selectionBox.lineWidth is in the passed-in options, perform the following assignment: + // selectionBox.lineWidth = max(0, floor(selectionBox.lineWidth)) + // + if (options.selectionBox) { + if (options.selectionBox.lineWidth) { + options.selectionBox.lineWidth = Math.floor(options.selectionBox.lineWidth); + if (options.selectionBox.lineWidth < 0) { + options.selectionBox.lineWidth = 0; + } + } + } + util.mergeOptions(this.options, options, "selectionBox"); } } diff --git a/lib/network/options.js b/lib/network/options.js index 5740d5a62..d86108106 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -170,6 +170,7 @@ let allOptions = { edges: { boolean: bool }, strokeStyle: { string }, fillStyle: { string }, + lineWidth: { number }, edgeAccuracy: { number }, __type__: { object, boolean: bool } }, @@ -586,6 +587,7 @@ let configureOptions = { edges: false, strokeStyle: "rgb(0,0,0)", fillStyle: "rgba(0,0,0,.0625)", + lineWidth: [2, 0, 8, 1], edgeAccuracy: [25, 1, 100, 1] }, hoverConnectedEdges: true, From 99a1b5e4b99ae85855f5d2db719a5cd3788542f0 Mon Sep 17 00:00:00 2001 From: softwareCobbler Date: Mon, 18 Feb 2019 18:15:45 -0500 Subject: [PATCH 10/10] Revert "fix failing test from ItemSet.js" This reverts commit f2bec3e6a28bd0e8e48dbbfaf90c3f886dfeaa5e (ref almende/vis PR #4256 --- lib/timeline/component/ItemSet.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 5e615b7af..485c5fe27 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -541,7 +541,6 @@ ItemSet.prototype.show = function() { /** * Activates the popup timer to show the given popup after a fixed time. - * @param {any} popup */ ItemSet.prototype.setPopupTimer = function (popup) { this.clearPopupTimer();