import OrderedIndexArray from "./OrderedIndexArray.js" /** * @typedef {import("../element/NodeElement.js").default} NodeElement * @typedef {typeof import("../Blueprint.js").default.nodeBoundariesSupplier} BoundariesFunction * @typedef {typeof import("../Blueprint.js").default.nodeSelectToggleFunction} SelectionFunction * @typedef {{ * primaryBoundary: Number, * secondaryBoundary: Number, * insertionPosition?: Number, * rectangle: Number * onSecondaryAxis: Boolean * }} Metadata */ export default class FastSelectionModel { /** * @param {Number[]} initialPosition * @param {NodeElement[]} rectangles * @param {BoundariesFunction} boundariesFunc * @param {SelectionFunction} selectFunc */ constructor(initialPosition, rectangles, boundariesFunc, selectFunc) { this.initialPosition = initialPosition this.finalPosition = initialPosition /** @type {Metadata[]} */ this.metadata = new Array(rectangles.length) this.primaryOrder = new OrderedIndexArray((element) => this.metadata[element].primaryBoundary) this.secondaryOrder = new OrderedIndexArray((element) => this.metadata[element].secondaryBoundary) this.selectFunc = selectFunc this.rectangles = rectangles this.primaryOrder.reserve(this.rectangles.length) this.secondaryOrder.reserve(this.rectangles.length) rectangles.forEach((rect, index) => { /** @type {Metadata} */ let rectangleMetadata = { primaryBoundary: this.initialPosition[0], secondaryBoundary: this.initialPosition[1], rectangle: index, onSecondaryAxis: false, } this.metadata[index] = rectangleMetadata selectFunc(rect, false) // Initially deselected (Eventually) const rectangleBoundaries = boundariesFunc(rect) // Secondary axis first because it may be inserted in this.secondaryOrder during the primary axis check if (this.initialPosition[1] < rectangleBoundaries.secondaryInf) { // Initial position is before the rectangle rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondaryInf } else if (rectangleBoundaries.secondarySup < this.initialPosition[1]) { // Initial position is after the rectangle rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondarySup } else { rectangleMetadata.onSecondaryAxis = true } if (this.initialPosition[0] < rectangleBoundaries.primaryInf) { // Initial position is before the rectangle rectangleMetadata.primaryBoundary = rectangleBoundaries.primaryInf this.primaryOrder.insert(index) } else if (rectangleBoundaries.primarySup < this.initialPosition[0]) { // Initial position is after the rectangle rectangleMetadata.primaryBoundary = rectangleBoundaries.primarySup this.primaryOrder.insert(index) } else { // Initial lays inside the rectangle (considering just this axis) // Secondary order depends on primary order, if primary boundaries are not satisfied, the element is not watched for secondary ones if (rectangleBoundaries.secondarySup < this.initialPosition[1] || this.initialPosition[1] < rectangleBoundaries.secondaryInf) { this.secondaryOrder.insert(index) } else { selectFunc(rect, true) } } }) this.primaryOrder.currentPosition = this.primaryOrder.getPosition(this.initialPosition[0]) this.secondaryOrder.currentPosition = this.secondaryOrder.getPosition(this.initialPosition[1]) this.computeBoundaries() } computeBoundaries() { this.boundaries = { // Primary axis negative expanding primaryN: { v: this.primaryOrder.getPrevValue(), i: this.primaryOrder.getPrev() }, primaryP: { v: this.primaryOrder.getNextValue(), i: this.primaryOrder.getNext() }, // Secondary axis negative expanding secondaryN: { v: this.secondaryOrder.getPrevValue(), i: this.secondaryOrder.getPrev() }, // Secondary axis positive expanding secondaryP: { v: this.secondaryOrder.getNextValue(), i: this.secondaryOrder.getNext() } } } selectTo(finalPosition) { const direction = [ Math.sign(finalPosition[0] - this.initialPosition[0]), Math.sign(finalPosition[1] - this.initialPosition[1]) ] const primaryBoundaryCrossed = (index, added) => { if (this.metadata[index].onSecondaryAxis) { this.selectFunc(this.rectangles[index], added) } else { if (added) { this.secondaryOrder.insert(index, finalPosition[1]) const secondaryBoundary = this.metadata[index].secondaryBoundary if ( // If inserted before the current position Math.sign(finalPosition[1] - secondaryBoundary) == direction[1] // And after initial position && Math.sign(secondaryBoundary - this.initialPosition[1]) == direction[1] ) { // Secondary axis is already satisfied then this.selectFunc(this.rectangles[index], true) } } else { this.selectFunc(this.rectangles[index], false) this.secondaryOrder.remove(index) } } this.computeBoundaries() this.selectTo(finalPosition) } if (finalPosition[0] < this.boundaries.primaryN.v) { --this.primaryOrder.currentPosition primaryBoundaryCrossed( this.boundaries.primaryN.i, this.initialPosition[0] > this.boundaries.primaryN.v && finalPosition[0] < this.initialPosition[0]) } else if (finalPosition[0] > this.boundaries.primaryP.v) { ++this.primaryOrder.currentPosition primaryBoundaryCrossed( this.boundaries.primaryP.i, this.initialPosition[0] < this.boundaries.primaryP.v && this.initialPosition[0] < finalPosition[0]) } const secondaryBoundaryCrossed = (index, added) => { this.selectFunc(this.rectangles[index], added) this.computeBoundaries() this.selectTo(finalPosition) } if (finalPosition[1] < this.boundaries.secondaryN.v) { --this.secondaryOrder.currentPosition secondaryBoundaryCrossed( this.boundaries.secondaryN.i, this.initialPosition[1] > this.boundaries.secondaryN.v && finalPosition[1] < this.initialPosition[1]) } else if (finalPosition[1] > this.boundaries.secondaryP.v) { ++this.secondaryOrder.currentPosition secondaryBoundaryCrossed( this.boundaries.secondaryP.i, this.initialPosition[1] < this.boundaries.secondaryP.v && this.initialPosition[1] < finalPosition[1]) } this.finalPosition = finalPosition } }