From 85748db201e954f2421446ae0f4b74303305b05c Mon Sep 17 00:00:00 2001 From: joostdecock Date: Mon, 12 Sep 2022 20:10:22 +0200 Subject: [PATCH] wip(core): Started working on support for stacks in core --- packages/core/src/part.mjs | 5 +- packages/core/src/pattern.mjs | 70 +++++++++++------ packages/core/src/stack.mjs | 82 +++++++++++++++++++ packages/core/src/store.mjs | 12 +-- packages/core/tests/part.test.mjs | 8 +- packages/core/tests/pattern-init.test.mjs | 2 +- packages/core/tests/stacks.test.mjs | 95 +++++++++++++++++++++++ 7 files changed, 238 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/stack.mjs create mode 100644 packages/core/tests/stacks.test.mjs diff --git a/packages/core/src/part.mjs b/packages/core/src/part.mjs index 99f12603d41..8e9517ba3fd 100644 --- a/packages/core/src/part.mjs +++ b/packages/core/src/part.mjs @@ -26,6 +26,7 @@ export function Part() { this.points = {} this.paths = {} this.snippets = {} + this.name = null return this } @@ -121,8 +122,8 @@ Part.prototype.boundary = function () { return this } -/** Stacks part so that its top left corner is in (0,0) */ -Part.prototype.stack = function () { +/** Homes part so that its top left corner is in (0,0) */ +Part.prototype.home = function () { if (this.topLeft !== false) return this else this.boundary() if (this.topLeft.x == 0 && this.topLeft.y == 0) return this diff --git a/packages/core/src/pattern.mjs b/packages/core/src/pattern.mjs index 29e98920012..5e5b1388252 100644 --- a/packages/core/src/pattern.mjs +++ b/packages/core/src/pattern.mjs @@ -9,6 +9,7 @@ import { mergeDependencies, } from './utils.mjs' import { Part } from './part.mjs' +import { Stack } from './stack.mjs' import { Point } from './point.mjs' import { Path } from './path.mjs' import { Snippet } from './snippet.mjs' @@ -23,7 +24,7 @@ export function Pattern(config) { addNonEnumProp(this, 'plugins', {}) addNonEnumProp(this, 'width', 0) addNonEnumProp(this, 'height', 0) - addNonEnumProp(this, 'autoLayout', { parts: {} }) + addNonEnumProp(this, 'autoLayout', { stacks: {} }) addNonEnumProp(this, 'is', '') addNonEnumProp(this, 'hooks', new Hooks()) addNonEnumProp(this, 'Point', Point) @@ -41,6 +42,7 @@ export function Pattern(config) { // Enumerable properties this.config = config // Design config this.parts = {} // Drafted parts container + this.stacks = {} // Drafted stacks container this.store = new Store() // Store for sharing data across parts return this @@ -86,6 +88,7 @@ Pattern.prototype.__createPartWithContext = function (name) { // Context object to add to Part closure const part = new Part() part.name = name + part.stack = this.__parts[name]?.stack || name part.context = { parts: this.parts, config: this.config, @@ -536,40 +539,44 @@ Pattern.prototype.macro = function (key, method) { this.macros[key] = method } -/** Packs parts in a 2D space and sets pattern size */ +/** Packs stacks in a 2D space and sets pattern size */ Pattern.prototype.pack = function () { if (this.store.logs.error.length > 0) { this.store.log.warning(`One or more errors occured. Not packing pattern parts`) return this } + // First, create all stacks + this.stacks = {} + for (const [name, part] of Object.entries(this.parts)) { + if (typeof this.stacks[part.stack] === 'undefined') this.stacks[part.stack] = new Stack(part.stack) + this.stacks[part.stack].addPart(part) + } + let bins = [] - for (let key in this.parts) { - let part = this.parts[key] - // Avoid multiple render calls to cause stacking of transforms - part.attributes.remove('transform') - if (part.render) { - part.stack() - let width = part.bottomRight.x - part.topLeft.x - let height = part.bottomRight.y - part.topLeft.y - if (this.settings.layout === true) bins.push({ id: key, width, height }) + for (const [key, stack] of Object.entries(this.stacks)) { + // Avoid multiple render calls to cause addition of transforms + stack.attributes.remove('transform') + if (!this.isStackHidden(key)) { + stack.home() + if (this.settings.layout === true) bins.push({ id: key, width: stack.width, height: stack.height }) else { - if (this.width < width) this.width = width - if (this.height < height) this.height = height + if (this.width < stack.width) this.width = stack.width + if (this.height < stack.height) this.height = stack.height } } } if (this.settings.layout === true) { let size = pack(bins, { inPlace: true }) for (let bin of bins) { - this.autoLayout.parts[bin.id] = { move: {} } - let part = this.parts[bin.id] + this.autoLayout.stacks[bin.id] = { move: {} } + let stack = this.stacks[bin.id] if (bin.x !== 0 || bin.y !== 0) { - part.attr('transform', `translate(${bin.x}, ${bin.y})`) + stack.attr('transform', `translate(${bin.x}, ${bin.y})`) } - this.autoLayout.parts[bin.id].move = { - x: bin.x + part.layout.move.x, - y: bin.y + part.layout.move.y, + this.autoLayout.stacks[bin.id].move = { + x: bin.x + stack.layout.move.x, + y: bin.y + stack.layout.move.y, } } this.width = size.width @@ -577,11 +584,11 @@ Pattern.prototype.pack = function () { } else if (typeof this.settings.layout === 'object') { this.width = this.settings.layout.width this.height = this.settings.layout.height - for (let partId of Object.keys(this.settings.layout.parts)) { + for (let stackId of Object.keys(this.settings.layout.stacks)) { // Some parts are added by late-stage plugins - if (this.parts[partId]) { - let transforms = this.settings.layout.parts[partId] - this.parts[partId].generateTransform(transforms) + if (this.stacks[stackId]) { + let transforms = this.settings.layout.stacks[stackId] + this.stacks[stackId].generateTransform(transforms) } } } @@ -783,6 +790,23 @@ Pattern.prototype.isHidden = function (partName) { return false } +/* Checks whether (all parts in) a stack is hidden in the config */ +Pattern.prototype.isStackHidden = function (stackName) { + if (!this.stacks[stackName]) return true + const parts = this.stacks[stackName].getPartNames() + if (Array.isArray(this.settings.only)) { + for (const partName of parts) { + if (this.settings.only.includes(partName)) return false + } + } + for (const partName of parts) { + if (this.__parts?.[partName]?.hide) return true + if (this.__parts?.[partName]?.hideAll) return true + } + + return false +} + /** Returns props required to render this pattern through * an external renderer (eg. a React component) */ diff --git a/packages/core/src/stack.mjs b/packages/core/src/stack.mjs new file mode 100644 index 00000000000..76aa73b382c --- /dev/null +++ b/packages/core/src/stack.mjs @@ -0,0 +1,82 @@ +import { Attributes } from './attributes.mjs' +import { Point } from './point.mjs' +import * as utils from './utils.mjs' + +export function Stack(name=null) { + // Non-enumerable properties + utils.addNonEnumProp(this, 'freeId', 0) + utils.addNonEnumProp(this, 'topLeft', false) + utils.addNonEnumProp(this, 'bottomRight', false) + utils.addNonEnumProp(this, 'width', false) + utils.addNonEnumProp(this, 'height', false) + utils.addNonEnumProp(this, 'layout', { move: { x: 0, y: 0 } }) + + // Enumerable properties + this.attributes = new Attributes() + this.parts = new Set() + this.name = name + + return this +} + +/* Adds a part to the stack */ +Stack.prototype.addPart = function(part) { + if (part) this.parts.add(part) + + return this +} + +/* Returns a list of parts in this stack */ +Stack.prototype.getPartList = function(part) { + return [...this.parts] +} + +/* Returns a list of names of parts in this stack */ +Stack.prototype.getPartNames = function(part) { + return [...this.parts].map(p => p.name) +} + +/** Homes the stack so that its top left corner is in (0,0) */ +Stack.prototype.home = function () { + const parts = this.getPartList() + if (parts.length < 1) return this + for (const part of this.getPartList()) { + part.home() + } + + if (parts.length === 1) { + this.topLeft = part.topLeft + this.bottomRigth = part.bottomRight + this.width = part.width + this.height = part.height + + return this + } + + return this.boundary() +} + +/** Calculates the stack's bounding box and sets it */ +Stack.prototype.home = function () { + this.topLeft = new Point(Infinity, Infinity) + this.bottomRight = new Point(-Infinity, -Infinity) + for (const part of this.getPartList()) { + if (part.topLeft.x < this.topLeft.x) this.topLeft.x = part.topLeft.x + if (part.topLeft.y < this.topLeft.y) this.topLeft.y = part.topLeft.y + if (part.bottomRight.x > this.bottomRight.x) this.bottomRight.x = part.bottomRight.x + if (part.bottomRight.y > this.bottomRight.y) this.bottomRight.y = part.bottomRight.y + } + + // Fix infinity if it's not overwritten + if (this.topLeft.x === Infinity) this.topLeft.x = 0 + if (this.topLeft.y === Infinity) this.topLeft.y = 0 + if (this.bottomRight.x === -Infinity) this.bottomRight.x = 0 + if (this.bottomRight.y === -Infinity) this.bottomRight.y = 0 + + this.width = this.bottomRight.x - this.topLeft.x + this.height = this.bottomRight.y - this.topLeft.y + + return this +} + +export default Stack diff --git a/packages/core/src/store.mjs b/packages/core/src/store.mjs index b1ff749afe3..bf162122916 100644 --- a/packages/core/src/store.mjs +++ b/packages/core/src/store.mjs @@ -5,12 +5,6 @@ import get from 'lodash.get' const avoid = ['set', 'setIfUnset', 'push', 'unset', 'get', 'extend'] export function Store(methods = []) { - for (const method of methods) { - if (avoid.indexOf(method[0]) !== -1) { - console.log(`WARNING: You can't squat ${method[0]}in the store`) - } else set(this, ...method) - } - /* * Default logging methods * You can override these with a plugin @@ -37,6 +31,12 @@ export function Store(methods = []) { } this.logs = logs + for (const method of methods) { + if (avoid.indexOf(method[0]) !== -1) { + this.logs.warning(`You cannot squat ${method[0]} in the store`) + } else set(this, ...method) + } + return this } diff --git a/packages/core/tests/part.test.mjs b/packages/core/tests/part.test.mjs index 659bd95c9ac..ce186ba6930 100644 --- a/packages/core/tests/part.test.mjs +++ b/packages/core/tests/part.test.mjs @@ -195,7 +195,7 @@ describe('Part', () => { const design = new Design({ parts: [ part ]}) const pattern = new design({ paperless: true }) pattern.draft() - pattern.parts.test.stack() + pattern.parts.test.home() console.log(pattern.parts.test.attributes) expect(part.attributes.get('transform')).to.equal('translate(-17, -74)') }) @@ -208,9 +208,9 @@ describe('Part', () => { part.points.from = new short.Point(2, 2) part.points.to = new short.Point(19, 76) part.paths.test = new short.Path().move(part.points.from).line(part.points.to) - part.stack() + part.home() expect(part.attributes.get('transform')).to.equal(false) - part.stack() + part.home() expect(part.attributes.get('transform')).to.equal(false) }) */ @@ -252,7 +252,7 @@ describe('Part', () => { part.points.from = new short.Point(2, 2) part.points.to = new short.Point(19, 76) part.paths.test = new short.Path().move(part.points.from).line(part.points.to) - part.stack() + part.home() part.generateTransform({ move: { x: 10, diff --git a/packages/core/tests/pattern-init.test.mjs b/packages/core/tests/pattern-init.test.mjs index 258ac0cb1d7..f1a094e9a2d 100644 --- a/packages/core/tests/pattern-init.test.mjs +++ b/packages/core/tests/pattern-init.test.mjs @@ -18,7 +18,7 @@ describe('Pattern', () => { expect(typeof pattern.config).to.equal('object') expect(typeof pattern.parts).to.equal('object') expect(typeof pattern.store).to.equal('object') - expect(Object.keys(pattern).length).to.equal(4) + expect(Object.keys(pattern).length).to.equal(5) }) it('Pattern constructor should add non-enumerable properties', () => { diff --git a/packages/core/tests/stacks.test.mjs b/packages/core/tests/stacks.test.mjs new file mode 100644 index 00000000000..e22a18cb85a --- /dev/null +++ b/packages/core/tests/stacks.test.mjs @@ -0,0 +1,95 @@ +import chai from 'chai' +import { round, Design, Point, pctBasedOn } from '../src/index.mjs' + +const expect = chai.expect + +describe('Stacks', () => { + + describe('Pattern.init()', () => { + const partA = { + name: 'test.partA', + measurements: ['head'], + options: { + size: { pct: 40, min: 20, max: 80 }, + }, + draft: ({ points, Point, paths, Path, part, store, measurements, options}) => { + store.set('size', measurements.head * options.size) + points.from = new Point(0,0) + points.to = new Point(0, store.get('size')) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + stack: 'box', + } + const partB = { + name: 'test.partB', + measurements: ['head'], + after: partA, + draft: ({ points, Point, paths, Path, part, store}) => { + points.from = new Point(0,store.get('size')) + points.to = new Point(store.get('size'), store.get('size')) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + stack: 'box', + } + const partC = { + name: 'test.partC', + after: partB, + draft: ({ points, Point, paths, Path, part, store}) => { + points.from = new Point(store.get('size'), store.get('size')) + points.to = new Point(store.get('size'), 0) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + stack: 'box', + } + const partD = { + name: 'test.partD', + after: partC, + draft: ({ points, Point, paths, Path, part, store}) => { + points.from = new Point(store.get('size'), 0) + points.to = new Point(0, 0) + paths.line = new Path().move(points.from).line(points.to) + return part + }, + // stack: 'box', + } + + const Pattern = new Design({ + data: { + name: 'test', + version: '1.2.3', + }, + parts: [partD], + }) + const pattern = new Pattern({ + measurements: { + head: 400 + } + }) + pattern.draft() + console.log(pattern.store.logs) + //console.log(pattern.parts) + //pattern.render() + console.log(pattern.render()) + + it('Pattern.init() should resolve dependencies', () => { + expect(typeof pattern.config.resolvedDependencies).to.equal('object') + expect(Array.isArray(pattern.config.resolvedDependencies['test.partA'])).to.equal(true) + expect(pattern.config.resolvedDependencies['test.partA'].length).to.equal(0) + expect(Array.isArray(pattern.config.resolvedDependencies['test.partB'])).to.equal(true) + expect(pattern.config.resolvedDependencies['test.partB'].length).to.equal(1) + expect(pattern.config.resolvedDependencies['test.partB'][0]).to.equal('test.partA') + expect(Array.isArray(pattern.config.resolvedDependencies['test.partC'])).to.equal(true) + expect(pattern.config.resolvedDependencies['test.partC'].length).to.equal(2) + expect( + pattern.config.resolvedDependencies['test.partC'].indexOf('test.partA') !== -1 + ).to.equal(true) + expect( + pattern.config.resolvedDependencies['test.partC'].indexOf('test.partB') !== -1 + ).to.equal(true) + }) + + }) +})