diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46f7d3c37c1..9a0eb29c0a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -106,8 +106,18 @@
- Added support for notes in flags
+### plugin-bin-pack
+
+#### Added
+
+ - First release of the plugin providing the default packing implementation
+
### core
+#### Added
+
+ - Allow plugins to provide their own packing implementation
+
#### Fixed
- Fix order in mergeOptions method so user settings take precendence over defaults
diff --git a/config/changelog.yaml b/config/changelog.yaml
index ba1e6690570..2fda3a4e227 100644
--- a/config/changelog.yaml
+++ b/config/changelog.yaml
@@ -1,4 +1,9 @@
Unreleased:
+ Added:
+ core:
+ - Allow plugins to provide their own packing implementation
+ plugin-bin-pack:
+ - First release of the plugin providing the default packing implementation
Changed:
aaron:
- Rephrased flag message when expand is off to avoid confusion about included seam allowance. Fixes #5057
diff --git a/config/dependencies.yaml b/config/dependencies.yaml
index c5e36264293..4cf68a59be6 100644
--- a/config/dependencies.yaml
+++ b/config/dependencies.yaml
@@ -109,6 +109,7 @@ core-plugins:
'@freesewing/plugin-mirror': *freesewing
'@freesewing/plugin-round': *freesewing
'@freesewing/plugin-sprinkle': *freesewing
+ '@freesewing/plugin-bin-pack': *freesewing
plugintest:
peer:
'@freesewing/plugin-annotations': *freesewing
diff --git a/config/software/plugins.json b/config/software/plugins.json
index 5b9a4e66ff3..a785f1c13c4 100644
--- a/config/software/plugins.json
+++ b/config/software/plugins.json
@@ -1,6 +1,7 @@
{
"core-plugins": "An umbrella package of essential plugins that are bundled with FreeSewing's core library",
"plugin-annotations": "A FreeSewing plugin that provides pattern annotations",
+ "plugin-bin-pack": "A FreeSewing plugin that adds a bin-pack algorithm to the core library",
"plugin-bust": "A FreeSewing plugin that helps with bust-adjusting menswear patterns",
"plugin-flip": "A FreeSewing plugin to flip parts horizontally",
"plugin-gore": "A FreeSewing plugin to generate gores for a semi-sphere or dome",
diff --git a/packages/core/src/pattern/index.mjs b/packages/core/src/pattern/index.mjs
index 5f1fd1f19e5..197ed0b718e 100644
--- a/packages/core/src/pattern/index.mjs
+++ b/packages/core/src/pattern/index.mjs
@@ -261,6 +261,18 @@ Pattern.prototype.__applySettings = function (sets) {
return this
}
+/**
+ * Populates the pattern store with methods set by plugins
+ *
+ * @private
+ * @return {Store} store - The pattern-wide store populated with relevant data/methods
+ */
+Pattern.prototype.__extendPatternStore = function () {
+ this.store.extend([...this.plugins.__storeMethods])
+
+ return this.store
+}
+
/**
* Creates a store for a set (of settings)
*
diff --git a/packages/core/src/pattern/pattern-drafter.mjs b/packages/core/src/pattern/pattern-drafter.mjs
index 78f8357cc74..25be54bea07 100644
--- a/packages/core/src/pattern/pattern-drafter.mjs
+++ b/packages/core/src/pattern/pattern-drafter.mjs
@@ -21,6 +21,9 @@ PatternDrafter.prototype.draft = function () {
// Keep container for drafted parts fresh
this.pattern.parts = []
+ // Extend pattern-wide store with methods from plugins
+ this.pattern.__extendPatternStore()
+
// Iterate over the provided sets of settings (typically just one)
for (const set in this.pattern.settings) {
this.pattern.setStores[set] = this.pattern.__createSetStore()
diff --git a/packages/core/src/pattern/pattern-plugins.mjs b/packages/core/src/pattern/pattern-plugins.mjs
index ab5e2341d47..b06e264a938 100644
--- a/packages/core/src/pattern/pattern-plugins.mjs
+++ b/packages/core/src/pattern/pattern-plugins.mjs
@@ -147,7 +147,6 @@ PatternPlugins.prototype.__loadPluginStoreMethods = function (plugin) {
for (const method of plugin.store) this.__storeMethods.add(method)
} else this.store.log.warn(`Plugin store methods should be an Array`)
- // console.log('store', plugin, this.__storeMethods)
return this
}
diff --git a/packages/core/src/pattern/pattern-renderer.mjs b/packages/core/src/pattern/pattern-renderer.mjs
index 4c02b27c8ec..13185d2574e 100644
--- a/packages/core/src/pattern/pattern-renderer.mjs
+++ b/packages/core/src/pattern/pattern-renderer.mjs
@@ -1,6 +1,5 @@
import { Svg } from '../svg.mjs'
import { Stack } from '../stack.mjs'
-import pack from 'bin-pack-with-constraints'
/**
* A class for handling layout and rendering for a pattern
@@ -107,8 +106,8 @@ PatternRenderer.prototype.__pack = function () {
}
}
if (settings[activeSet].layout === true) {
- // some plugins will add a width constraint to the settings, but we can safely pass undefined if not
- let size = pack(bins, { inPlace: true, maxWidth: settings[0].maxWidth })
+ // store.pack is provided by a plugin
+ const size = bins.length > 0 ? this.pattern.store.pack(bins, this) : { width: 0, height: 0 }
this.autoLayout.width = size.width
this.autoLayout.height = size.height
diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs
index d428f4b3d17..8330677d2b8 100644
--- a/packages/core/src/store.mjs
+++ b/packages/core/src/store.mjs
@@ -50,6 +50,9 @@ export function Store(methods = []) {
} else set(this, path, method)
}
+ // Fallback packing algorithm
+ this.pack = fallbackPacker
+
return this
}
@@ -156,3 +159,22 @@ Store.prototype.unset = function (path) {
return this
}
+
+/**
+ * The default pack method comes from a plugin, typically
+ * plugin-bin-back which is part of core plugins.
+ * However, when a pattern is loaded without plugins
+ * we stil don't want it work even when no pack method
+ * is available, so this is the fallback default pack method.
+ */
+function fallbackPacker(items, pattern) {
+ console.log({ items, pattern })
+ let w = 0
+ let h = 0
+ for (const item of items) {
+ if (item.width > w) w = item.width
+ if (item.height > w) w = item.height
+ }
+
+ return { w, h }
+}
diff --git a/packages/core/tests/svg.test.mjs b/packages/core/tests/svg.test.mjs
index 51023f54d95..1ce919f98e6 100644
--- a/packages/core/tests/svg.test.mjs
+++ b/packages/core/tests/svg.test.mjs
@@ -5,6 +5,7 @@ import { Design, Attributes } from '../src/index.mjs'
import { Defs } from '../src/defs.mjs'
import { version } from '../data.mjs'
import render from './fixtures/render.mjs'
+import { binpackPlugin } from '../../../plugins/plugin-bin-pack/src/index.mjs'
chai.use(chaiString)
const expect = chai.expect
@@ -12,6 +13,7 @@ const expect = chai.expect
const getPattern = (settings = {}, draft = false) => {
const part = {
name: 'test',
+ plugins: binpackPlugin,
draft: draft
? draft
: ({ paths, Path, Point, part }) => {
diff --git a/plugins/core-plugins/package.json b/plugins/core-plugins/package.json
index 4083a7c645f..8ea49a34d07 100644
--- a/plugins/core-plugins/package.json
+++ b/plugins/core-plugins/package.json
@@ -58,7 +58,8 @@
"@freesewing/plugin-annotations": "3.0.0",
"@freesewing/plugin-mirror": "3.0.0",
"@freesewing/plugin-round": "3.0.0",
- "@freesewing/plugin-sprinkle": "3.0.0"
+ "@freesewing/plugin-sprinkle": "3.0.0",
+ "@freesewing/plugin-bin-pack": "3.0.0"
},
"files": [
"dist/*",
diff --git a/plugins/core-plugins/src/index.mjs b/plugins/core-plugins/src/index.mjs
index f3fc1a4d279..a95d734e4d8 100644
--- a/plugins/core-plugins/src/index.mjs
+++ b/plugins/core-plugins/src/index.mjs
@@ -3,6 +3,7 @@ import { measurementsPlugin } from '../../plugin-measurements/src/index.mjs'
import { mirrorPlugin } from '../../plugin-mirror/src/index.mjs'
import { roundPlugin } from '../../plugin-round/src/index.mjs'
import { sprinklePlugin } from '../../plugin-sprinkle/src/index.mjs'
+import { binpackPlugin } from '../../plugin-bin-pack/src/index.mjs'
import { name, version } from '../data.mjs'
const bundledPlugins = [
@@ -11,6 +12,7 @@ const bundledPlugins = [
mirrorPlugin,
roundPlugin,
sprinklePlugin,
+ binpackPlugin,
]
const hooks = {}
diff --git a/plugins/plugin-bin-pack/CHANGELOG.md b/plugins/plugin-bin-pack/CHANGELOG.md
new file mode 100644
index 00000000000..70b7bbfbb45
--- /dev/null
+++ b/plugins/plugin-bin-pack/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Change log for: @freesewing/plugin-bin-pack
+
+
+## 3.0.0 (2022-09-30)
+
+### Changed
+
+ - All FreeSewing pacakges are now ESM only.
+ - All FreeSewing pacakges now use named exports.
+ - Dropped support for NodeJS 14. NodeJS 18 (LTS/hydrogen) or more recent is now required.
+
+
+This is the **initial release**, and the start of this change log.
+
+> Prior to version 2, FreeSewing was not a JavaScript project.
+> As such, that history is out of scope for this change log.
+
diff --git a/plugins/plugin-bin-pack/README.md b/plugins/plugin-bin-pack/README.md
new file mode 100644
index 00000000000..10dc0a6465a
--- /dev/null
+++ b/plugins/plugin-bin-pack/README.md
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+# @freesewing/plugin-bin-pack
+
+A FreeSewing plugin that adds a bin-pack algorithm to the core library
+
+
+
+
+## What am I looking at? 🤔
+
+This repository is the FreeSewing *monorepo* holding all FreeSewing's websites, documentation, designs, plugins, and other NPM packages.
+
+This folder holds: @freesewing/plugin-bin-pack
+
+If you're not entirely sure what to do or how to start, type this command:
+
+```
+npm run tips
+```
+
+> If you don't want to set up a dev environment, you can run it in your browser:
+>
+> [](https://gitpod.io/#https://github.com/freesewing/freesewing)
+>
+> We recommend that you fork our repository and then
+> put `gitpod.io/# to start up a browser-based dev environment of your own.
+
+## About FreeSewing 💀
+
+Where the world of makers and developers collide, that's where you'll find FreeSewing.
+
+If you're a maker, checkout [freesewing.org](https://freesewing.org/) where you can generate
+sewing patterns adapted to your measurements.
+
+If you're a developer, the FreeSewing documentation lives at [freesewing.dev](https://freesewing.dev/).
+The FreeSewing [core library](https://freesewing.dev/reference/api/) is a *batteries-included* toolbox
+for parametric design of sewing patterns. But FreeSewing also provides a range
+of [plugins](https://freesewing.dev/reference/plugins/) that further extend the
+functionality of the platform.
+
+If you have NodeJS installed, you can try it right now by running:
+
+```bash
+npx create-freesewing-pattern
+```
+
+Getting started guides are available for:
+- [Linux](https://freesewing.dev/tutorials/getting-started-linux/)
+- [MacOS](https://freesewing.dev/tutorials/getting-started-mac/)
+- [Windows](https://freesewing.dev/tutorials/getting-started-windows/)
+
+The [pattern design tutorial](https://freesewing.dev/tutorials/pattern-design/) will
+show you how to create your first parametric design.
+
+## Support FreeSewing: Become a patron 🥰
+
+FreeSewing is an open source project maintained by Joost De Cock and financially supported by the FreeSewing patrons.
+
+If you feel FreeSewing is worthwhile, and you can spend a few coind without
+hardship, then you should [join us and become a patron](https://freesewing.org/community/join).
+
+## Links 👩💻
+
+**Official channels**
+
+ - 💻 Makers website: [FreeSewing.org](https://freesewing.org)
+ - 💻 Developers website: [FreeSewing.dev](https://freesewing.dev)
+ - ✅ [Support](https://github.com/freesewing/freesewing/issues/new/choose),
+ [Issues](https://github.com/freesewing/freesewing/issues) &
+ [Discussions](https://github.com/freesewing/freesewing/discussions) on
+ [GitHub](https://github.com/freesewing/freesewing)
+
+**Social media**
+
+ - 🐦 Twitter: [@freesewing_org](https://twitter.com/freesewing_org)
+ - 📷 Instagram: [@freesewing_org](https://instagram.com/freesewing_org)
+
+**Places the FreeSewing community hangs out**
+
+ - 💬 [Discord](https://discord.freesewing.org/)
+ - 💬 [Facebook](https://www.facebook.com/groups/627769821272714/)
+ - 💬 [Reddit](https://www.reddit.com/r/freesewing/)
+
+## License: MIT 🤓
+
+© [Joost De Cock](https://github.com/joostdecock).
+See [the license file](https://github.com/freesewing/freesewing/blob/develop/LICENSE) for details.
+
+## Where to get help 🤯
+
+For [Support](https://github.com/freesewing/freesewing/issues/new/choose),
+please use the [Issues](https://github.com/freesewing/freesewing/issues) &
+[Discussions](https://github.com/freesewing/freesewing/discussions) on
+[GitHub](https://github.com/freesewing/freesewing).
+
diff --git a/plugins/plugin-bin-pack/build.mjs b/plugins/plugin-bin-pack/build.mjs
new file mode 100644
index 00000000000..99ace216bc8
--- /dev/null
+++ b/plugins/plugin-bin-pack/build.mjs
@@ -0,0 +1,35 @@
+/* This script will build the package with esbuild */
+import esbuild from 'esbuild'
+import pkg from './package.json' assert { type: 'json' }
+
+// Create banner based on package info
+const banner = `/**
+ * ${pkg.name} | v${pkg.version}
+ * ${pkg.description}
+ * (c) ${new Date().getFullYear()} ${pkg.author}
+ * @license ${pkg.license}
+ */`
+
+// Shared esbuild options
+const options = {
+ banner: { js: banner },
+ bundle: true,
+ entryPoints: ['src/index.mjs'],
+ format: 'esm',
+ outfile: 'dist/index.mjs',
+ external: ['@freesewing'],
+ metafile: process.env.VERBOSE ? true : false,
+ minify: process.env.NO_MINIFY ? false : true,
+ sourcemap: true,
+}
+
+// Let esbuild generate the build
+const build = async () => {
+ const result = await esbuild.build(options).catch(() => process.exit(1))
+
+ if (process.env.VERBOSE) {
+ const info = await esbuild.analyzeMetafile(result.metafile)
+ console.log(info)
+ }
+}
+build()
diff --git a/plugins/plugin-bin-pack/data.mjs b/plugins/plugin-bin-pack/data.mjs
new file mode 100644
index 00000000000..6f3f67732a5
--- /dev/null
+++ b/plugins/plugin-bin-pack/data.mjs
@@ -0,0 +1,4 @@
+// This file is auto-generated | All changes you make will be overwritten.
+export const name = '@freesewing/plugin-bin-pack'
+export const version = '3.0.0'
+export const data = { name, version }
diff --git a/plugins/plugin-bin-pack/package.json b/plugins/plugin-bin-pack/package.json
new file mode 100644
index 00000000000..7a958afe472
--- /dev/null
+++ b/plugins/plugin-bin-pack/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@freesewing/plugin-bin-pack",
+ "version": "3.0.0",
+ "description": "A FreeSewing plugin that adds a bin-pack algorithm to the core library",
+ "author": "Joost De Cock (https://github.com/joostdecock)",
+ "homepage": "https://freesewing.org/",
+ "repository": "github:freesewing/freesewing",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/freesewing/freesewing/issues"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://freesewing.org/patrons/join"
+ },
+ "keywords": [
+ "freesewing",
+ "plugin",
+ "sewing pattern",
+ "sewing",
+ "design",
+ "parametric design",
+ "made to measure",
+ "diy",
+ "fashion"
+ ],
+ "type": "module",
+ "module": "dist/index.mjs",
+ "exports": {
+ ".": {
+ "internal": "./src/index.mjs",
+ "default": "./dist/index.mjs"
+ }
+ },
+ "scripts": {
+ "build": "node build.mjs",
+ "build:all": "yarn build",
+ "clean": "rimraf dist",
+ "mbuild": "NO_MINIFY=1 node build.mjs",
+ "symlink": "mkdir -p ./node_modules/@freesewing && cd ./node_modules/@freesewing && ln -s -f ../../../* . && cd -",
+ "test": "npx mocha tests/*.test.mjs",
+ "vbuild": "VERBOSE=1 node build.mjs",
+ "lab": "cd ../../sites/lab && yarn start",
+ "tips": "node ../../scripts/help.mjs",
+ "lint": "npx eslint 'src/**' 'tests/*.mjs'",
+ "prettier": "npx prettier --write 'src/*.mjs' 'tests/*.mjs'",
+ "testci": "NODE_OPTIONS=\"--conditions=internal\" npx mocha tests/*.test.mjs --reporter ../../tests/reporters/terse.js",
+ "wbuild": "node build.mjs",
+ "wbuild:all": "yarn wbuild"
+ },
+ "peerDependencies": {
+ "@freesewing/core": "3.0.0"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "mocha": "10.2.0",
+ "chai": "4.3.10"
+ },
+ "files": [
+ "dist/*",
+ "README.md"
+ ],
+ "publishConfig": {
+ "access": "public",
+ "tag": "latest"
+ },
+ "engines": {
+ "node": "18",
+ "npm": "9"
+ }
+}
diff --git a/plugins/plugin-bin-pack/src/growing-packer.mjs b/plugins/plugin-bin-pack/src/growing-packer.mjs
new file mode 100644
index 00000000000..01fca368887
--- /dev/null
+++ b/plugins/plugin-bin-pack/src/growing-packer.mjs
@@ -0,0 +1,239 @@
+/******************************************************************************
+
+This is a binary tree based bin packing algorithm that is more complex than
+the simple Packer (packer.js). Instead of starting off with a fixed width and
+height, it starts with the width and height of the first block passed and then
+grows as necessary to accomodate each subsequent block.
+
+The default behavior as it grows is that it attempts
+to maintain a roughly square ratio by making 'smart' choices about whether to
+grow right or down.
+
+If either a maxWidth or a maxHeight is set, it will try not to grow beyond that
+limit, and choose instead to grow in the other direction. If the first block is
+larger than the given limit, however, that block's dimension will replace the
+limit.
+
+When growing, the algorithm can only grow to the right OR down. Therefore, if
+the new block is BOTH wider and taller than the current target then it will be
+rejected. This makes it very important to initialize with a sensible starting
+width and height. If you are providing sorted input (largest first) then this
+will not be an issue.
+
+A potential way to solve this limitation would be to allow growth in BOTH
+directions at once, but this requires maintaining a more complex tree
+with 3 children (down, right and center) and that complexity can be avoided
+by simply chosing a sensible starting block.
+
+Best results occur when the input blocks are sorted by height, or even better
+when sorted by max(width,height).
+
+Construction:
+------------
+ {
+ maxWidth = Infinity: set a max width to constrain the growth in that direction
+ maxHeight = Infinity: set a max height to constrain the growth in that direction
+ strictMax = false: require wider-than-max blocks to start at left boundary
+ PREVIOUS INTENDED TODO strictMax = false: reject blocks that are larger than the max
+ }
+
+Inputs:
+------
+
+ blocks: array of any objects that have .w and .h attributes
+
+
+Outputs:
+-------
+
+ marks each block that fits with a .fit attribute pointing to a
+ node with .x and .y coordinates
+
+Example:
+-------
+
+ let blocks = [
+ { w: 100, h: 100 },
+ { w: 100, h: 100 },
+ { w: 80, h: 80 },
+ { w: 80, h: 80 },
+ etc
+ etc
+ ];
+
+ let packer = new GrowingPacker();
+ packer.fit(blocks);
+
+ for(let n = 0 ; n < blocks.length ; n++) {
+ let block = blocks[n]
+ if (block.fit) {
+ Draw(block.fit.x, block.fit.y, block.w, block.h)
+ }
+ }
+
+
+******************************************************************************/
+
+const GrowingPacker = function ({ maxWidth = Infinity, maxHeight = Infinity, strictMax = false }) {
+ this.maxWidth = maxWidth
+ this.maxHeight = maxHeight
+ this.strictMax = strictMax
+ this.ensureSquare = maxWidth === Infinity && maxHeight === Infinity
+}
+
+GrowingPacker.prototype = {
+ fit: function (blocks) {
+ let len = blocks.length
+ if (len === 0) {
+ return
+ }
+
+ let n, node, block, fit
+ // Require wider-than-max blocks to start at the left boundary, so
+ // they encroach past the right boundary minimally.
+ let width =
+ this.strictMax && this.maxWidth < Infinity
+ ? Math.max(blocks[0].width, this.maxWidth)
+ : blocks[0].width
+
+ let height = this.strictMax && this.maxHeight < Infinity ? this.maxHeight : blocks[0].height
+ this.root = { x: 0, y: 0, width, height }
+ for (n = 0; n < len; n++) {
+ block = blocks[n]
+ if ((node = this.findNode(this.root, block.width, block.height))) {
+ fit = this.splitNode(node, block.width, block.height)
+ block.x = fit.x
+ block.y = fit.y
+ } else {
+ fit = this.growNode(block.width, block.height)
+ block.x = fit.x
+ block.y = fit.y
+ }
+ }
+ },
+
+ findNode: function (root, width, height) {
+ if (root.used)
+ return this.findNode(root.right, width, height) || this.findNode(root.down, width, height)
+ else if (width <= root.width && height <= root.height) return root
+ else return null
+ },
+
+ splitNode: function (node, width, height) {
+ node.used = true
+ const downY = node.y + height
+ node.down = {
+ x: node.x,
+ y: downY,
+ width: node.width,
+ height: Math.min(node.height - height, this.maxHeight - downY),
+ }
+
+ const rightX = node.x + width
+ node.right = {
+ x: rightX,
+ y: node.y,
+ width: Math.min(node.width - width, this.maxWidth - rightX),
+ height: height,
+ }
+ return node
+ },
+
+ growNode: function (width, height) {
+ let canGrowDown = width <= this.root.width
+ let canGrowRight = height <= this.root.height
+
+ const proposedNewWidth = this.root.width + width
+ let shouldGrowRight =
+ canGrowRight && (this.ensureSquare ? this.root.height : this.maxWidth) >= proposedNewWidth // attempt to keep square-ish by growing right when height is much greater than width
+ const proposedNewHeight = this.root.height + height
+ let shouldGrowDown =
+ canGrowDown && (this.ensureSquare ? this.root.width : this.maxHeight) >= proposedNewHeight // attempt to keep square-ish by growing down when width is much greater than height
+
+ if (shouldGrowRight) return this.growRight(width, height)
+ else if (shouldGrowDown) return this.growDown(width, height)
+ else if (canGrowRight) return this.growRight(width, height)
+ else if (canGrowDown) return this.growDown(width, height)
+ else return null // need to ensure sensible root starting size to avoid this happening
+ },
+
+ growRight: function (width, height) {
+ this.root = {
+ used: true,
+ x: 0,
+ y: 0,
+ width: this.root.width + width,
+ height: this.root.height,
+ down: this.root,
+ right: { x: this.root.width, y: 0, width: width, height: this.root.height },
+ }
+ let node
+ if ((node = this.findNode(this.root, width, height))) return this.splitNode(node, width, height)
+ else return null
+ },
+
+ growDown: function (width, height) {
+ this.root = {
+ used: true,
+ x: 0,
+ y: 0,
+ width: this.root.width,
+ height: this.root.height + height,
+ down: { x: 0, y: this.root.height, width: this.root.width, height: height },
+ right: this.root,
+ }
+ let node
+ if ((node = this.findNode(this.root, width, height))) return this.splitNode(node, width, height)
+ else return null
+ },
+}
+
+const defaultOptions = { inPlace: true }
+
+export const pack = (store, items, pattern) => {
+ const options = {
+ ...defaultOptions,
+ ...pattern.pattern.settings[0],
+ }
+ const packer = new GrowingPacker(options)
+ const inPlace = options.inPlace || false
+
+ // Clone the items.
+ let newItems = items.map(function (item) {
+ return inPlace ? item : { width: item.width, height: item.height, item: item }
+ })
+
+ // We need to know whether the widest item exceeds the maximum width.
+ // The optimal sorting strategy changes depending on this.
+ const widestWidth = newItems.sort(function (a, b) {
+ return b.width - a.width
+ })[0].width
+ const widerThanMax = options.maxWidth && widestWidth > options.maxWidth
+
+ newItems = newItems.sort(function (a, b) {
+ if (options.maxWidth && !options.maxHeight && widerThanMax) return b.width - a.width
+ if (options.maxWidth && !options.maxHeight) return b.height - a.height
+ if (options.maxHeight) return b.height - a.height
+ // TODO: check that each actually HAS a width and a height.
+ // Sort based on the size (area) of each block.
+ return b.width * b.height - a.width * a.height
+ })
+
+ packer.fit(newItems)
+
+ const w = newItems.reduce(function (curr, item) {
+ return Math.max(curr, item.x + item.width)
+ }, 0)
+ const h = newItems.reduce(function (curr, item) {
+ return Math.max(curr, item.y + item.height)
+ }, 0)
+
+ const ret = {
+ width: w,
+ height: h,
+ }
+
+ if (!inPlace) ret.items = newItems
+
+ return ret
+}
diff --git a/plugins/plugin-bin-pack/src/index.mjs b/plugins/plugin-bin-pack/src/index.mjs
new file mode 100644
index 00000000000..5409fe1f21b
--- /dev/null
+++ b/plugins/plugin-bin-pack/src/index.mjs
@@ -0,0 +1,13 @@
+import { name, version } from '../data.mjs'
+import { pack } from './growing-packer.mjs'
+
+export const plugin = {
+ name,
+ version,
+ store: [['pack', pack]],
+}
+
+// More specifically named exports
+export const packPlugin = plugin
+export const binPackPlugin = plugin
+export const binpackPlugin = plugin
diff --git a/plugins/plugin-bin-pack/tests/shared.test.mjs b/plugins/plugin-bin-pack/tests/shared.test.mjs
new file mode 100644
index 00000000000..7bf4f668398
--- /dev/null
+++ b/plugins/plugin-bin-pack/tests/shared.test.mjs
@@ -0,0 +1,6 @@
+// This file is auto-generated | Any changes you make will be overwritten.
+import { plugin } from '../src/index.mjs'
+import { sharedPluginTests } from '../../../tests/plugins/shared.mjs'
+
+// Run shared tests
+sharedPluginTests(plugin)