From b83ab5df74d8a2214e9f64a9dc9d7a714bf09736 Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Sun, 15 Oct 2023 15:12:10 +0200 Subject: [PATCH] feat(core): Allow plugins to provide their own packing implementation --- CHANGELOG.md | 10 + config/changelog.yaml | 5 + config/dependencies.yaml | 1 + config/software/plugins.json | 1 + packages/core/src/pattern/index.mjs | 12 + packages/core/src/pattern/pattern-drafter.mjs | 3 + packages/core/src/pattern/pattern-plugins.mjs | 1 - .../core/src/pattern/pattern-renderer.mjs | 5 +- packages/core/src/store.mjs | 22 ++ packages/core/tests/svg.test.mjs | 2 + plugins/core-plugins/package.json | 3 +- plugins/core-plugins/src/index.mjs | 2 + plugins/plugin-bin-pack/CHANGELOG.md | 17 ++ plugins/plugin-bin-pack/README.md | 143 +++++++++++ plugins/plugin-bin-pack/build.mjs | 35 +++ plugins/plugin-bin-pack/data.mjs | 4 + plugins/plugin-bin-pack/package.json | 71 ++++++ .../plugin-bin-pack/src/growing-packer.mjs | 239 ++++++++++++++++++ plugins/plugin-bin-pack/src/index.mjs | 13 + plugins/plugin-bin-pack/tests/shared.test.mjs | 6 + 20 files changed, 590 insertions(+), 5 deletions(-) create mode 100644 plugins/plugin-bin-pack/CHANGELOG.md create mode 100644 plugins/plugin-bin-pack/README.md create mode 100644 plugins/plugin-bin-pack/build.mjs create mode 100644 plugins/plugin-bin-pack/data.mjs create mode 100644 plugins/plugin-bin-pack/package.json create mode 100644 plugins/plugin-bin-pack/src/growing-packer.mjs create mode 100644 plugins/plugin-bin-pack/src/index.mjs create mode 100644 plugins/plugin-bin-pack/tests/shared.test.mjs 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](https://static.freesewing.org/banner.png) +

@freesewing/plugin-bin-pack on NPM + License: MIT + Code quality on DeepScan + Open issues tagged pkg:plugin-bin-pack + All Contributors +

Follow @freesewing_org on Twitter + Chat with us on Discord + Become a FreeSewing Patron + Follow @freesewing_org on Twitter +

+ +# @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: +> +> [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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)