Files
ueblueprint/js/SelectionModel.js
2021-09-30 17:37:26 +02:00

140 lines
6.8 KiB
JavaScript

import OrderedIndexArray from "./OrderedIndexArray.js"
export default class SelectionModel {
/**
* @typedef {{
* primaryInf: number,
* primarySup: number,
* secondaryInf: number,
* secondarySup: number
* }} BoundariesInfo
* @typedef {{
* primaryBoundary: number,
* secondaryBoundary: number,
* insertionPosition: number,
* rectangle: number
* onSecondaryAxis: Boolean
* }} Metadata
* @typedef {numeric} Rectangle
* @param {number[]} initialPosition Coordinates of the starting point of selection [primaryAxisValue, secondaryAxisValue].
* @param {Rectangle[]} rectangles Rectangles that can be selected by this object.
* @param {(rect: Rectangle) => BoundariesInfo} boundariesFunc A function that, given a rectangle, it provides the boundaries of such rectangle.
* @param {(rect: Rectangle, selected: bool) => void} selectToggleFunction A function that selects or deselects individual rectangles.
*/
constructor(initialPosition, rectangles, boundariesFunc, selectToggleFunction) {
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.selectToggleFunction = selectToggleFunction
this.rectangles = rectangles
rectangles.forEach((rect, index) => {
/** @type Metadata */
let rectangleMetadata = {
primaryBoundary: this.initialPosition[0],
secondaryBoundary: this.initialPosition[1],
rectangle: index, // used to move both directions inside the this.metadata array
onSecondaryAxis: false
}
selectToggleFunction(rect, false) // Initially deselected (Eventually)
const rectangleBoundaries = boundariesFunc(rect)
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 {
selectToggleFunction(rect, true)
}
}
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
}
this.metadata[index] = rectangleMetadata
})
this.primaryOrder.current = this.primaryOrder.getPosition(this.initialPosition[0])
this.secondaryOrder.current = this.secondaryOrder.getPosition(this.initialPosition[1])
this.computeBoundaries(this.initialPosition)
}
computeBoundaries() {
this.boundaries = {
// Primary axis negative direction
primaryN: {
'value': this.primaryOrder.getPrev(),
'index': this.primaryOrder.current - 1
},
primaryP: {
'value': this.primaryOrder.getNext(),
'index': this.primaryOrder.current
},
// Secondary axis negative direction
secondaryN: {
'value': this.secondaryOrder.getPrev(),
'index': this.secondaryOrder.current - 1
},
// Secondary axis positive direction
secondaryP: {
'value': this.secondaryOrder.getNext(),
'index': this.secondaryOrder.current
}
}
}
selectTo(finalPosition) {
const primaryBoundaryCrossed = (index, extended) => {
if (extended) {
this.primaryOrder.current += Math.sign(finalPosition[0] - this.initialPosition[0])
if (this.metadata[index].onSecondaryAxis) {
this.selectToggleFunction(this.rectangles[index], true)
} else {
this.secondaryOrder.insert(index, this.initialPosition[1])
}
} else {
this.primaryOrder.current -= Math.sign(finalPosition[0] - this.initialPosition[0])
this.secondaryOrder.remove(index)
this.selectToggleFunction(this.rectangles[index], false)
}
this.computeBoundaries(finalPosition)
this.selectTo(finalPosition)
}
if (finalPosition[0] < this.boundaries.primaryN.value) {
primaryBoundaryCrossed(this.boundaries.primaryN.index, finalPosition[0] < this.initialPosition[0])
} else if (finalPosition[0] > this.boundaries.primaryP.value) {
primaryBoundaryCrossed(this.boundaries.primaryP.index, this.initialPosition[0] < finalPosition[0])
}
const secondaryBoundaryCrossed = (index, extended) => {
if (extended) {
this.secondaryOrder.current += Math.sign(finalPosition[1] - this.initialPosition[1])
} else {
this.secondaryOrder.current -= Math.sign(finalPosition[1] - this.initialPosition[1])
}
this.selectToggleFunction(this.rectangles[index], extended)
this.computeBoundaries(finalPosition)
this.selectTo(finalPosition)
}
if (finalPosition[1] < this.boundaries.secondaryN.value) {
secondaryBoundaryCrossed(this.boundaries.secondaryN.index, finalPosition[1] < this.initialPosition[1]);
} else if (finalPosition[1] > this.boundaries.secondaryP.value) {
secondaryBoundaryCrossed(this.boundaries.secondaryP.index, this.initialPosition[1] < finalPosition[1]);
}
this.finalPosition = finalPosition
}
}