feat(core): Allow plugins to provide their own packing implementation
This commit is contained in:
parent
e9135f707a
commit
b83ab5df74
20 changed files with 590 additions and 5 deletions
10
CHANGELOG.md
10
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
*
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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/*",
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
17
plugins/plugin-bin-pack/CHANGELOG.md
Normal file
17
plugins/plugin-bin-pack/CHANGELOG.md
Normal file
|
@ -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.
|
||||
|
143
plugins/plugin-bin-pack/README.md
Normal file
143
plugins/plugin-bin-pack/README.md
Normal file
|
@ -0,0 +1,143 @@
|
|||

|
||||
<p align='center'><a
|
||||
href="https://www.npmjs.com/package/@freesewing/plugin-bin-pack"
|
||||
title="@freesewing/plugin-bin-pack on NPM"
|
||||
><img src="https://img.shields.io/npm/v/@freesewing/plugin-bin-pack.svg"
|
||||
alt="@freesewing/plugin-bin-pack on NPM"/>
|
||||
</a><a
|
||||
href="https://opensource.org/licenses/MIT"
|
||||
title="License: MIT"
|
||||
><img src="https://img.shields.io/npm/l/@freesewing/plugin-bin-pack.svg?label=License"
|
||||
alt="License: MIT"/>
|
||||
</a><a
|
||||
href="https://deepscan.io/dashboard#view=project&tid=2114&pid=2993&bid=23256"
|
||||
title="Code quality on DeepScan"
|
||||
><img src="https://deepscan.io/api/teams/2114/projects/2993/branches/23256/badge/grade.svg"
|
||||
alt="Code quality on DeepScan"/>
|
||||
</a><a
|
||||
href="https://github.com/freesewing/freesewing/issues?q=is%3Aissue+is%3Aopen+label%3Apkg%3Aplugin-bin-pack"
|
||||
title="Open issues tagged pkg:plugin-bin-pack"
|
||||
><img src="https://img.shields.io/github/issues/freesewing/freesewing/pkg:plugin-bin-pack.svg?label=Issues"
|
||||
alt="Open issues tagged pkg:plugin-bin-pack"/>
|
||||
</a><a
|
||||
href="#contributors-"
|
||||
title="All Contributors"
|
||||
><img src="https://img.shields.io/badge/all_contributors-111-pink.svg"
|
||||
alt="All Contributors"/>
|
||||
</a></p><p align='center'><a
|
||||
href="https://twitter.com/freesewing_org"
|
||||
title="Follow @freesewing_org on Twitter"
|
||||
><img src="https://img.shields.io/badge/%F3%A0%80%A0-Follow%20us-blue.svg?logo=twitter&logoColor=white&logoWidth=15"
|
||||
alt="Follow @freesewing_org on Twitter"/>
|
||||
</a><a
|
||||
href="https://chat.freesewing.org"
|
||||
title="Chat with us on Discord"
|
||||
><img src="https://img.shields.io/discord/698854858052075530?label=Chat%20on%20Discord"
|
||||
alt="Chat with us on Discord"/>
|
||||
</a><a
|
||||
href="https://freesewing.org/patrons/join"
|
||||
title="Become a FreeSewing Patron"
|
||||
><img src="https://img.shields.io/badge/%F3%A0%80%A0-Support%20us-blueviolet.svg?logo=cash-app&logoColor=white&logoWidth=15"
|
||||
alt="Become a FreeSewing Patron"/>
|
||||
</a><a
|
||||
href="https://instagram.com/freesewing_org"
|
||||
title="Follow @freesewing_org on Twitter"
|
||||
><img src="https://img.shields.io/badge/%F3%A0%80%A0-Follow%20us-E4405F.svg?logo=instagram&logoColor=white&logoWidth=15"
|
||||
alt="Follow @freesewing_org on Twitter"/>
|
||||
</a></p>
|
||||
|
||||
# @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/#<entire-url-of-your-fork` into a browser
|
||||
> 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).
|
||||
|
35
plugins/plugin-bin-pack/build.mjs
Normal file
35
plugins/plugin-bin-pack/build.mjs
Normal file
|
@ -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()
|
4
plugins/plugin-bin-pack/data.mjs
Normal file
4
plugins/plugin-bin-pack/data.mjs
Normal file
|
@ -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 }
|
71
plugins/plugin-bin-pack/package.json
Normal file
71
plugins/plugin-bin-pack/package.json
Normal file
|
@ -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 <joost@joost.at> (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"
|
||||
}
|
||||
}
|
239
plugins/plugin-bin-pack/src/growing-packer.mjs
Normal file
239
plugins/plugin-bin-pack/src/growing-packer.mjs
Normal file
|
@ -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
|
||||
}
|
13
plugins/plugin-bin-pack/src/index.mjs
Normal file
13
plugins/plugin-bin-pack/src/index.mjs
Normal file
|
@ -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
|
6
plugins/plugin-bin-pack/tests/shared.test.mjs
Normal file
6
plugins/plugin-bin-pack/tests/shared.test.mjs
Normal file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue