diff --git a/package-lock.json b/package-lock.json index ae6c9d0dca5..a2bb1de08a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -836,6 +836,11 @@ "live-server": "1.2.0" } }, + "bin-pack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bin-pack/-/bin-pack-1.0.2.tgz", + "integrity": "sha1-wqAU7b8L7XCjKSBi7UZXe5YSBnk=" + }, "binary-extensions": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", diff --git a/package.json b/package.json index 89b9c157e7f..61c84d539f0 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "bezier-js": "^2.2.13", + "bin-pack": "1.0.2", "hooks": "^0.3.2" }, "devDependencies": { diff --git a/src/part.js b/src/part.js index 7322e1354d5..b65c2888c5a 100644 --- a/src/part.js +++ b/src/part.js @@ -13,6 +13,8 @@ function part(id) { this.snippets = {}; this.id = id; this.freeId = 0; + this.topLeft = false; + this.bottomRight = false; this.render = id.substr(0, 1) === "_" ? false : true; this.points.origin = new point(0, 0); for (let k in hooklib) this[k] = hooklib[k]; @@ -50,8 +52,52 @@ part.prototype.getUid = function() { return "" + this.freeId; }; +/** Returns a value formatted for units provided in settings */ part.prototype.units = function(value) { return units(value, this.context.settings.units); }; +/** Calculates the part's bounding box and sets it */ +part.prototype.boundary = function() { + if (this.topLeft) return this; // Cached + + let topLeft = new point(Infinity, Infinity); + let bottomRight = new point(-Infinity, -Infinity); + for (let key in this.paths) { + let path = this.paths[key].boundary(); + if (path.render) { + if (path.topLeft.x < topLeft.x) topLeft.x = path.topLeft.x; + if (path.topLeft.y < topLeft.y) topLeft.y = path.topLeft.y; + if (path.bottomRight.x > bottomRight.x) + bottomRight.x = path.bottomRight.x; + if (path.bottomRight.y > bottomRight.y) + bottomRight.y = path.bottomRight.y; + } + } + // Add 10mm margin + this.topLeft = new point(topLeft.x - 10, topLeft.y - 10); + this.bottomRight = new point(bottomRight.x + 10, bottomRight.y + 10); + + return this; +}; + +/** Stacks part so that its top left corner is in (0,0) */ +part.prototype.stack = function() { + if (this.topLeft.x === 0 && this.topLeft.y === 0) return this; + + this.boundary().attr( + "transform", + `translate(${this.topLeft.x * -1}, ${this.topLeft.y * -1})` + ); + + return this; +}; + +/** Adds an attribute. This is here to make this call chainable in assignment */ +part.prototype.attr = function(name, value) { + this.attributes.add(name, value); + + return this; +}; + export default part; diff --git a/src/path.js b/src/path.js index d11b4c05d61..5b7215abb7c 100644 --- a/src/path.js +++ b/src/path.js @@ -1,9 +1,12 @@ import attributes from "./attributes"; +import point from "./point"; import Bezier from "bezier-js"; import { pathOffset, pathLength } from "./utils"; function path() { this.render = true; + this.topLeft = false; + this.bottomRight = false; this.attributes = new attributes(); this.ops = []; } @@ -114,4 +117,39 @@ path.prototype.end = function() { if (op.type === "close") return this.start(); else return op.to; }; + +/** Finds the bounding box of a path */ +path.prototype.boundary = function() { + if (this.topLeft) return this; // Cached + + let current; + let topLeft = new point(Infinity, Infinity); + let bottomRight = new point(-Infinity, -Infinity); + for (let i in this.ops) { + let op = this.ops[i]; + if (op.type === "move" || op.type === "line") { + if (op.to.x < topLeft.x) topLeft.x = op.to.x; + if (op.to.y < topLeft.y) topLeft.y = op.to.y; + if (op.to.x > bottomRight.x) bottomRight.x = op.to.x; + if (op.to.y > bottomRight.y) bottomRight.y = op.to.y; + } else if (op.type === "curve") { + let bb = new Bezier( + { x: current.x, y: current.y }, + { x: op.cp1.x, y: op.cp1.y }, + { x: op.cp2.x, y: op.cp2.y }, + { x: op.to.x, y: op.to.y } + ).bbox(); + if (bb.x.min < topLeft.x) topLeft.x = bb.x.min; + if (bb.y.min < topLeft.y) topLeft.y = bb.y.min; + if (bb.x.max > bottomRight.x) bottomRight.x = bb.x.max; + if (bb.y.max > bottomRight.y) bottomRight.y = bb.y.max; + } + if (op.to) current = op.to; + } + + this.topLeft = topLeft; + this.bottomRight = bottomRight; + + return this; +}; export default path; diff --git a/src/pattern.js b/src/pattern.js index bcdcb3ab947..2c125b728b7 100644 --- a/src/pattern.js +++ b/src/pattern.js @@ -1,3 +1,4 @@ +import attributes from "./attributes"; import { macroName } from "./utils"; import part from "./part"; import point from "./point"; @@ -5,6 +6,7 @@ import path from "./path"; import snippet from "./snippet"; import svg from "./svg"; import hooks from "./hooks"; +import pack from "bin-pack"; export default function pattern(config = false) { // Allow no-config patterns @@ -26,6 +28,8 @@ export default function pattern(config = false) { throw "Could not create pattern: You should define at least one part in your pattern config"; } + this.width = false; + this.height = false; // Constructors this.point = point; this.path = path; @@ -77,6 +81,7 @@ pattern.prototype.draft = function() { pattern.prototype.render = function() { this.hooks.attach("preRenderSvg", this.svg); + this.hooks.attach("postRenderSvg", this.svg); //this.hooks.attach('insertText', this.svg); @@ -122,3 +127,29 @@ pattern.prototype.macro = function(key, method) { this.hooks.attach(name, part); } }; + +/** Packs parts in a 2D space and sets pattern size */ +pattern.prototype.pack = function() { + let bins = []; + for (let key in this.parts) { + let part = this.parts[key]; + if (part.render) { + part.stack(); + bins.push({ + id: part.id, + width: part.bottomRight.x - part.topLeft.x, + height: part.bottomRight.y - part.topLeft.y + }); + } + } + let size = pack(bins, { inPlace: true }); + for (let bin of bins) { + let part = this.parts[bin.id]; + if (bin.x !== 0 || bin.y !== 0) + part.attr("transform", `translate (${bin.x}, ${bin.y})`); + } + this.width = size.width; + this.height = size.height; + + return this; +}; diff --git a/src/svg.js b/src/svg.js index 19dbcface08..7b1e7f4dace 100644 --- a/src/svg.js +++ b/src/svg.js @@ -41,6 +41,11 @@ svg.prototype.insertText = function() {}; /** Renders a draft object as SVG */ svg.prototype.render = function(pattern) { this.preRenderSvg(); + // this needs to run after the preSvgRender hook as it might add stuff + pattern.pack(); + this.attributes.add("width", pattern.width + "mm"); + this.attributes.add("height", pattern.height + "mm"); + this.attributes.add("viewBox", `0 0 ${pattern.width} ${pattern.height}`); this.svg = this.prefix; this.svg += this.renderComments(this.header); this.svg += this.renderSvgTag(pattern); @@ -204,11 +209,13 @@ svg.prototype.renderSnippet = function(snippet) { }; /** Returns SVG code to open a group */ -svg.prototype.openGroup = function(id) { +svg.prototype.openGroup = function(id, attributes = false) { let svg = this.nl() + this.nl(); svg += ``; svg += this.nl(); - svg += ``; + svg += `