This commit is contained in:
yinsx
2026-02-02 09:07:30 +08:00
parent e04353a5fa
commit bf5a3bc343
78 changed files with 11771 additions and 318 deletions

View 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);
}