1
This commit is contained in:
597
app/src/vendor/muuri-src/Packer/PackerProcessor.js
vendored
Normal file
597
app/src/vendor/muuri-src/Packer/PackerProcessor.js
vendored
Normal file
@ -0,0 +1,597 @@
|
||||
/**
|
||||
* Muuri Packer
|
||||
* Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
|
||||
* Released under the MIT license
|
||||
* https://github.com/haltu/muuri/blob/master/src/Packer/LICENSE.md
|
||||
*/
|
||||
|
||||
function createPackerProcessor(isWorker) {
|
||||
var FILL_GAPS = 1;
|
||||
var HORIZONTAL = 2;
|
||||
var ALIGN_RIGHT = 4;
|
||||
var ALIGN_BOTTOM = 8;
|
||||
var ROUNDING = 16;
|
||||
|
||||
var EPS = 0.001;
|
||||
var MIN_SLOT_SIZE = 0.5;
|
||||
|
||||
// Rounds number first to three decimal precision and then floors the result
|
||||
// to two decimal precision.
|
||||
// Math.floor(Math.round(number * 1000) / 10) / 100
|
||||
function roundNumber(number) {
|
||||
return ((((number * 1000 + 0.5) << 0) / 10) << 0) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
function PackerProcessor() {
|
||||
this.currentRects = [];
|
||||
this.nextRects = [];
|
||||
this.rectTarget = {};
|
||||
this.rectStore = [];
|
||||
this.slotSizes = [];
|
||||
this.rectId = 0;
|
||||
this.slotIndex = -1;
|
||||
this.slotData = { left: 0, top: 0, width: 0, height: 0 };
|
||||
this.sortRectsLeftTop = this.sortRectsLeftTop.bind(this);
|
||||
this.sortRectsTopLeft = this.sortRectsTopLeft.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a layout object as an argument and computes positions (slots) for the
|
||||
* layout items. Also computes the final width and height of the layout. The
|
||||
* provided layout object's slots array is mutated as well as the width and
|
||||
* height properties.
|
||||
*
|
||||
* @param {Object} layout
|
||||
* @param {Number} layout.width
|
||||
* - The start (current) width of the layout in pixels.
|
||||
* @param {Number} layout.height
|
||||
* - The start (current) height of the layout in pixels.
|
||||
* @param {(Item[]|Number[])} layout.items
|
||||
* - List of Muuri.Item instances or a list of item dimensions
|
||||
* (e.g [ item1Width, item1Height, item2Width, item2Height, ... ]).
|
||||
* @param {(Array|Float32Array)} layout.slots
|
||||
* - An Array/Float32Array instance which's length should equal to
|
||||
* the amount of items times two. The position (width and height) of each
|
||||
* item will be written into this array.
|
||||
* @param {Number} settings
|
||||
* - The layout's settings as bitmasks.
|
||||
* @returns {Object}
|
||||
*/
|
||||
PackerProcessor.prototype.computeLayout = function (layout, settings) {
|
||||
var items = layout.items;
|
||||
var slots = layout.slots;
|
||||
var fillGaps = !!(settings & FILL_GAPS);
|
||||
var horizontal = !!(settings & HORIZONTAL);
|
||||
var alignRight = !!(settings & ALIGN_RIGHT);
|
||||
var alignBottom = !!(settings & ALIGN_BOTTOM);
|
||||
var rounding = !!(settings & ROUNDING);
|
||||
var isPreProcessed = typeof items[0] === 'number';
|
||||
var i, bump, item, slotWidth, slotHeight, slot;
|
||||
|
||||
// No need to go further if items do not exist.
|
||||
if (!items.length) return layout;
|
||||
|
||||
// Compute slots for the items.
|
||||
bump = isPreProcessed ? 2 : 1;
|
||||
for (i = 0; i < items.length; i += bump) {
|
||||
// If items are pre-processed it means that items array contains only
|
||||
// the raw dimensions of the items. Otherwise we assume it is an array
|
||||
// of normal Muuri items.
|
||||
if (isPreProcessed) {
|
||||
slotWidth = items[i];
|
||||
slotHeight = items[i + 1];
|
||||
} else {
|
||||
item = items[i];
|
||||
slotWidth = item._width + item._marginLeft + item._marginRight;
|
||||
slotHeight = item._height + item._marginTop + item._marginBottom;
|
||||
}
|
||||
|
||||
// If rounding is enabled let's round the item's width and height to
|
||||
// make the layout algorithm a bit more stable. This has a performance
|
||||
// cost so don't use this if not necessary.
|
||||
if (rounding) {
|
||||
slotWidth = roundNumber(slotWidth);
|
||||
slotHeight = roundNumber(slotHeight);
|
||||
}
|
||||
|
||||
// Get slot data.
|
||||
slot = this.computeNextSlot(layout, slotWidth, slotHeight, fillGaps, horizontal);
|
||||
|
||||
// Update layout width/height.
|
||||
if (horizontal) {
|
||||
if (slot.left + slot.width > layout.width) {
|
||||
layout.width = slot.left + slot.width;
|
||||
}
|
||||
} else {
|
||||
if (slot.top + slot.height > layout.height) {
|
||||
layout.height = slot.top + slot.height;
|
||||
}
|
||||
}
|
||||
|
||||
// Add item slot data to layout slots.
|
||||
slots[++this.slotIndex] = slot.left;
|
||||
slots[++this.slotIndex] = slot.top;
|
||||
|
||||
// Store the size too (for later usage) if needed.
|
||||
if (alignRight || alignBottom) {
|
||||
this.slotSizes.push(slot.width, slot.height);
|
||||
}
|
||||
}
|
||||
|
||||
// If the alignment is set to right we need to adjust the results.
|
||||
if (alignRight) {
|
||||
for (i = 0; i < slots.length; i += 2) {
|
||||
slots[i] = layout.width - (slots[i] + this.slotSizes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// If the alignment is set to bottom we need to adjust the results.
|
||||
if (alignBottom) {
|
||||
for (i = 1; i < slots.length; i += 2) {
|
||||
slots[i] = layout.height - (slots[i] + this.slotSizes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset stuff.
|
||||
this.slotSizes.length = 0;
|
||||
this.currentRects.length = 0;
|
||||
this.nextRects.length = 0;
|
||||
this.rectStore.length = 0;
|
||||
this.rectId = 0;
|
||||
this.slotIndex = -1;
|
||||
|
||||
return layout;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate next slot in the layout. Returns a slot object with position and
|
||||
* dimensions data. The returned object is reused between calls.
|
||||
*
|
||||
* @param {Object} layout
|
||||
* @param {Number} slotWidth
|
||||
* @param {Number} slotHeight
|
||||
* @param {Boolean} fillGaps
|
||||
* @param {Boolean} horizontal
|
||||
* @returns {Object}
|
||||
*/
|
||||
PackerProcessor.prototype.computeNextSlot = function (
|
||||
layout,
|
||||
slotWidth,
|
||||
slotHeight,
|
||||
fillGaps,
|
||||
horizontal
|
||||
) {
|
||||
var slot = this.slotData;
|
||||
var currentRects = this.currentRects;
|
||||
var nextRects = this.nextRects;
|
||||
var ignoreCurrentRects = false;
|
||||
var rect;
|
||||
var rectId;
|
||||
var shards;
|
||||
var i;
|
||||
var j;
|
||||
|
||||
// Reset new slots.
|
||||
nextRects.length = 0;
|
||||
|
||||
// Set item slot initial data.
|
||||
slot.left = null;
|
||||
slot.top = null;
|
||||
slot.width = slotWidth;
|
||||
slot.height = slotHeight;
|
||||
|
||||
// Try to find position for the slot from the existing free spaces in the
|
||||
// layout.
|
||||
for (i = 0; i < currentRects.length; i++) {
|
||||
rectId = currentRects[i];
|
||||
if (!rectId) continue;
|
||||
rect = this.getRect(rectId);
|
||||
if (slot.width <= rect.width + EPS && slot.height <= rect.height + EPS) {
|
||||
slot.left = rect.left;
|
||||
slot.top = rect.top;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no position was found for the slot let's position the slot to
|
||||
// the bottom left (in vertical mode) or top right (in horizontal mode) of
|
||||
// the layout.
|
||||
if (slot.left === null) {
|
||||
if (horizontal) {
|
||||
slot.left = layout.width;
|
||||
slot.top = 0;
|
||||
} else {
|
||||
slot.left = 0;
|
||||
slot.top = layout.height;
|
||||
}
|
||||
|
||||
// If gaps don't need filling let's throw away all the current free spaces
|
||||
// (currentRects).
|
||||
if (!fillGaps) {
|
||||
ignoreCurrentRects = true;
|
||||
}
|
||||
}
|
||||
|
||||
// In vertical mode, if the slot's bottom overlaps the layout's bottom.
|
||||
if (!horizontal && slot.top + slot.height > layout.height + EPS) {
|
||||
// If slot is not aligned to the left edge, create a new free space to the
|
||||
// left of the slot.
|
||||
if (slot.left > MIN_SLOT_SIZE) {
|
||||
nextRects.push(this.addRect(0, layout.height, slot.left, Infinity));
|
||||
}
|
||||
|
||||
// If slot is not aligned to the right edge, create a new free space to
|
||||
// the right of the slot.
|
||||
if (slot.left + slot.width < layout.width - MIN_SLOT_SIZE) {
|
||||
nextRects.push(
|
||||
this.addRect(
|
||||
slot.left + slot.width,
|
||||
layout.height,
|
||||
layout.width - slot.left - slot.width,
|
||||
Infinity
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update layout height.
|
||||
layout.height = slot.top + slot.height;
|
||||
}
|
||||
|
||||
// In horizontal mode, if the slot's right overlaps the layout's right edge.
|
||||
if (horizontal && slot.left + slot.width > layout.width + EPS) {
|
||||
// If slot is not aligned to the top, create a new free space above the
|
||||
// slot.
|
||||
if (slot.top > MIN_SLOT_SIZE) {
|
||||
nextRects.push(this.addRect(layout.width, 0, Infinity, slot.top));
|
||||
}
|
||||
|
||||
// If slot is not aligned to the bottom, create a new free space below
|
||||
// the slot.
|
||||
if (slot.top + slot.height < layout.height - MIN_SLOT_SIZE) {
|
||||
nextRects.push(
|
||||
this.addRect(
|
||||
layout.width,
|
||||
slot.top + slot.height,
|
||||
Infinity,
|
||||
layout.height - slot.top - slot.height
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update layout width.
|
||||
layout.width = slot.left + slot.width;
|
||||
}
|
||||
|
||||
// Clean up the current free spaces making sure none of them overlap with
|
||||
// the slot. Split all overlapping free spaces into smaller shards that do
|
||||
// not overlap with the slot.
|
||||
if (!ignoreCurrentRects) {
|
||||
if (fillGaps) i = 0;
|
||||
for (; i < currentRects.length; i++) {
|
||||
rectId = currentRects[i];
|
||||
if (!rectId) continue;
|
||||
rect = this.getRect(rectId);
|
||||
shards = this.splitRect(rect, slot);
|
||||
for (j = 0; j < shards.length; j++) {
|
||||
rectId = shards[j];
|
||||
rect = this.getRect(rectId);
|
||||
// Make sure that the free space is within the boundaries of the
|
||||
// layout. This routine is critical to the algorithm as it makes sure
|
||||
// that there are no leftover spaces with infinite height/width.
|
||||
// It's also essential that we don't compare values absolutely to each
|
||||
// other but leave a little headroom (EPSILON) to get rid of false
|
||||
// positives.
|
||||
if (
|
||||
horizontal ? rect.left + EPS < layout.width - EPS : rect.top + EPS < layout.height - EPS
|
||||
) {
|
||||
nextRects.push(rectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize and sort all the new free spaces that will be used in the next
|
||||
// iteration. This procedure is critical to make the bin-packing algorithm
|
||||
// work. The free spaces have to be in correct order in the beginning of the
|
||||
// next iteration.
|
||||
if (nextRects.length > 1) {
|
||||
this.purgeRects(nextRects).sort(horizontal ? this.sortRectsLeftTop : this.sortRectsTopLeft);
|
||||
}
|
||||
|
||||
// Finally we need to make sure that `this.currentRects` points to
|
||||
// `nextRects` array as that is used in the next iteration's beginning when
|
||||
// we try to find a space for the next slot.
|
||||
this.currentRects = nextRects;
|
||||
this.nextRects = currentRects;
|
||||
|
||||
return slot;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new rectangle to the rectangle store. Returns the id of the new
|
||||
* rectangle.
|
||||
*
|
||||
* @param {Number} left
|
||||
* @param {Number} top
|
||||
* @param {Number} width
|
||||
* @param {Number} height
|
||||
* @returns {Number}
|
||||
*/
|
||||
PackerProcessor.prototype.addRect = function (left, top, width, height) {
|
||||
var rectId = ++this.rectId;
|
||||
this.rectStore[rectId] = left || 0;
|
||||
this.rectStore[++this.rectId] = top || 0;
|
||||
this.rectStore[++this.rectId] = width || 0;
|
||||
this.rectStore[++this.rectId] = height || 0;
|
||||
return rectId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get rectangle data from the rectangle store by id. Optionally you can
|
||||
* provide a target object where the rectangle data will be written in. By
|
||||
* default an internal object is reused as a target object.
|
||||
*
|
||||
* @param {Number} id
|
||||
* @param {Object} [target]
|
||||
* @returns {Object}
|
||||
*/
|
||||
PackerProcessor.prototype.getRect = function (id, target) {
|
||||
if (!target) target = this.rectTarget;
|
||||
target.left = this.rectStore[id] || 0;
|
||||
target.top = this.rectStore[++id] || 0;
|
||||
target.width = this.rectStore[++id] || 0;
|
||||
target.height = this.rectStore[++id] || 0;
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Punch a hole into a rectangle and return the shards (1-4).
|
||||
*
|
||||
* @param {Object} rect
|
||||
* @param {Object} hole
|
||||
* @returns {Number[]}
|
||||
*/
|
||||
PackerProcessor.prototype.splitRect = (function () {
|
||||
var shards = [];
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
return function (rect, hole) {
|
||||
// Reset old shards.
|
||||
shards.length = 0;
|
||||
|
||||
// If the slot does not overlap with the hole add slot to the return data
|
||||
// as is. Note that in this case we are eager to keep the slot as is if
|
||||
// possible so we use the EPSILON in favour of that logic.
|
||||
if (
|
||||
rect.left + rect.width <= hole.left + EPS ||
|
||||
hole.left + hole.width <= rect.left + EPS ||
|
||||
rect.top + rect.height <= hole.top + EPS ||
|
||||
hole.top + hole.height <= rect.top + EPS
|
||||
) {
|
||||
shards.push(this.addRect(rect.left, rect.top, rect.width, rect.height));
|
||||
return shards;
|
||||
}
|
||||
|
||||
// Left split.
|
||||
width = hole.left - rect.left;
|
||||
if (width >= MIN_SLOT_SIZE) {
|
||||
shards.push(this.addRect(rect.left, rect.top, width, rect.height));
|
||||
}
|
||||
|
||||
// Right split.
|
||||
width = rect.left + rect.width - (hole.left + hole.width);
|
||||
if (width >= MIN_SLOT_SIZE) {
|
||||
shards.push(this.addRect(hole.left + hole.width, rect.top, width, rect.height));
|
||||
}
|
||||
|
||||
// Top split.
|
||||
height = hole.top - rect.top;
|
||||
if (height >= MIN_SLOT_SIZE) {
|
||||
shards.push(this.addRect(rect.left, rect.top, rect.width, height));
|
||||
}
|
||||
|
||||
// Bottom split.
|
||||
height = rect.top + rect.height - (hole.top + hole.height);
|
||||
if (height >= MIN_SLOT_SIZE) {
|
||||
shards.push(this.addRect(rect.left, hole.top + hole.height, rect.width, height));
|
||||
}
|
||||
|
||||
return shards;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Check if a rectangle is fully within another rectangle.
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
PackerProcessor.prototype.isRectAWithinRectB = function (a, b) {
|
||||
return (
|
||||
a.left + EPS >= b.left &&
|
||||
a.top + EPS >= b.top &&
|
||||
a.left + a.width - EPS <= b.left + b.width &&
|
||||
a.top + a.height - EPS <= b.top + b.height
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops through an array of rectangle ids and resets all that are fully
|
||||
* within another rectangle in the array. Resetting in this case means that
|
||||
* the rectangle id value is replaced with zero.
|
||||
*
|
||||
* @param {Number[]} rectIds
|
||||
* @returns {Number[]}
|
||||
*/
|
||||
PackerProcessor.prototype.purgeRects = (function () {
|
||||
var rectA = {};
|
||||
var rectB = {};
|
||||
return function (rectIds) {
|
||||
var i = rectIds.length;
|
||||
var j;
|
||||
|
||||
while (i--) {
|
||||
j = rectIds.length;
|
||||
if (!rectIds[i]) continue;
|
||||
this.getRect(rectIds[i], rectA);
|
||||
while (j--) {
|
||||
if (!rectIds[j] || i === j) continue;
|
||||
this.getRect(rectIds[j], rectB);
|
||||
if (this.isRectAWithinRectB(rectA, rectB)) {
|
||||
rectIds[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rectIds;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Sort rectangles with top-left gravity.
|
||||
*
|
||||
* @param {Number} aId
|
||||
* @param {Number} bId
|
||||
* @returns {Number}
|
||||
*/
|
||||
PackerProcessor.prototype.sortRectsTopLeft = (function () {
|
||||
var rectA = {};
|
||||
var rectB = {};
|
||||
return function (aId, bId) {
|
||||
this.getRect(aId, rectA);
|
||||
this.getRect(bId, rectB);
|
||||
|
||||
return rectA.top < rectB.top && rectA.top + EPS < rectB.top
|
||||
? -1
|
||||
: rectA.top > rectB.top && rectA.top - EPS > rectB.top
|
||||
? 1
|
||||
: rectA.left < rectB.left && rectA.left + EPS < rectB.left
|
||||
? -1
|
||||
: rectA.left > rectB.left && rectA.left - EPS > rectB.left
|
||||
? 1
|
||||
: 0;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Sort rectangles with left-top gravity.
|
||||
*
|
||||
* @param {Number} aId
|
||||
* @param {Number} bId
|
||||
* @returns {Number}
|
||||
*/
|
||||
PackerProcessor.prototype.sortRectsLeftTop = (function () {
|
||||
var rectA = {};
|
||||
var rectB = {};
|
||||
return function (aId, bId) {
|
||||
this.getRect(aId, rectA);
|
||||
this.getRect(bId, rectB);
|
||||
return rectA.left < rectB.left && rectA.left + EPS < rectB.left
|
||||
? -1
|
||||
: rectA.left > rectB.left && rectA.left - EPS < rectB.left
|
||||
? 1
|
||||
: rectA.top < rectB.top && rectA.top + EPS < rectB.top
|
||||
? -1
|
||||
: rectA.top > rectB.top && rectA.top - EPS > rectB.top
|
||||
? 1
|
||||
: 0;
|
||||
};
|
||||
})();
|
||||
|
||||
if (isWorker) {
|
||||
var PACKET_INDEX_WIDTH = 1;
|
||||
var PACKET_INDEX_HEIGHT = 2;
|
||||
var PACKET_INDEX_OPTIONS = 3;
|
||||
var PACKET_HEADER_SLOTS = 4;
|
||||
var processor = new PackerProcessor();
|
||||
|
||||
self.onmessage = function (msg) {
|
||||
var data = new Float32Array(msg.data);
|
||||
var items = data.subarray(PACKET_HEADER_SLOTS, data.length);
|
||||
var slots = new Float32Array(items.length);
|
||||
var settings = data[PACKET_INDEX_OPTIONS];
|
||||
var layout = {
|
||||
items: items,
|
||||
slots: slots,
|
||||
width: data[PACKET_INDEX_WIDTH],
|
||||
height: data[PACKET_INDEX_HEIGHT],
|
||||
};
|
||||
|
||||
// Compute the layout (width / height / slots).
|
||||
processor.computeLayout(layout, settings);
|
||||
|
||||
// Copy layout data to the return data.
|
||||
data[PACKET_INDEX_WIDTH] = layout.width;
|
||||
data[PACKET_INDEX_HEIGHT] = layout.height;
|
||||
data.set(layout.slots, PACKET_HEADER_SLOTS);
|
||||
|
||||
// Send layout back to the main thread.
|
||||
postMessage(data.buffer, [data.buffer]);
|
||||
};
|
||||
}
|
||||
|
||||
return PackerProcessor;
|
||||
}
|
||||
|
||||
var PackerProcessor = createPackerProcessor();
|
||||
export default PackerProcessor;
|
||||
|
||||
//
|
||||
// WORKER UTILS
|
||||
//
|
||||
|
||||
var blobUrl = null;
|
||||
var activeWorkers = [];
|
||||
|
||||
export function createWorkerProcessors(amount, onmessage) {
|
||||
var workers = [];
|
||||
|
||||
if (amount > 0) {
|
||||
if (!blobUrl) {
|
||||
blobUrl = URL.createObjectURL(
|
||||
new Blob(['(' + createPackerProcessor.toString() + ')(true)'], {
|
||||
type: 'application/javascript',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (var i = 0, worker; i < amount; i++) {
|
||||
worker = new Worker(blobUrl);
|
||||
if (onmessage) worker.onmessage = onmessage;
|
||||
workers.push(worker);
|
||||
activeWorkers.push(worker);
|
||||
}
|
||||
}
|
||||
|
||||
return workers;
|
||||
}
|
||||
|
||||
export function destroyWorkerProcessors(workers) {
|
||||
var worker;
|
||||
var index;
|
||||
|
||||
for (var i = 0; i < workers.length; i++) {
|
||||
worker = workers[i];
|
||||
worker.onmessage = null;
|
||||
worker.onerror = null;
|
||||
worker.onmessageerror = null;
|
||||
worker.terminate();
|
||||
|
||||
index = activeWorkers.indexOf(worker);
|
||||
if (index > -1) activeWorkers.splice(index, 1);
|
||||
}
|
||||
|
||||
if (blobUrl && !activeWorkers.length) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
blobUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWorkerProcessorsSupported() {
|
||||
return !!(window.Worker && window.URL && window.Blob);
|
||||
}
|
||||
Reference in New Issue
Block a user