From 4f39f5e357f49d028b323b949e0bd3a5b0f5840a Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sat, 14 Jul 2018 16:04:39 +0000 Subject: [PATCH] :construction: Working on rendering --- config/config.ts | 3 ++ index.ts | 14 +++++-- lib/attributes.ts | 20 ++++++++++ lib/part.ts | 6 ++- lib/path.ts | 78 ++++++++++++++++++++++++++++++++++++ lib/pattern.ts | 10 ++++- lib/point.ts | 4 +- lib/renderer.ts | 96 +++++++++++++++++++++++++++++++++++++++++++++ lib/utils.ts | 47 ++++++++++++++++++++++ tests/utils.test.js | 77 ++++++++++++++++++++++++++++++++++++ 10 files changed, 347 insertions(+), 8 deletions(-) create mode 100644 config/config.ts create mode 100644 lib/attributes.ts create mode 100644 lib/path.ts create mode 100644 lib/renderer.ts create mode 100644 tests/utils.test.js diff --git a/config/config.ts b/config/config.ts new file mode 100644 index 00000000000..ffc07605908 --- /dev/null +++ b/config/config.ts @@ -0,0 +1,3 @@ +export var config = { + precision: 2 +} diff --git a/index.ts b/index.ts index 111f8b4d36b..011eec6d081 100644 --- a/index.ts +++ b/index.ts @@ -1,12 +1,18 @@ import { Pattern } from './lib/pattern' import { Point } from './lib/point' +import { Path } from './lib/path' +import { config } from './config/config' +import * as utils from './lib/utils' import bezier from 'bezier-js' var Freesewing = { - version: '0.0.1', - pattern: Pattern, - point: Point, - bezier + version: '1.0.1', + config, + pattern: Pattern, + point: Point, + path: Path, + utils, + bezier } export default Freesewing; diff --git a/lib/attributes.ts b/lib/attributes.ts new file mode 100644 index 00000000000..88c5b584c6e --- /dev/null +++ b/lib/attributes.ts @@ -0,0 +1,20 @@ +export class Attributes { + list: {name: string, value: string}[] = []; + + /** Adds an attribute */ + add(name: string, value: string): Attributes { + this.list.push({name, value}); + + return this; + } + + /** Returns SVG code for attributes */ + render(): string { + let svg = ''; + for (let a of this.list) { + svg += ` ${a.name}="${a.value}"`; + } + + return svg; + } +} diff --git a/lib/part.ts b/lib/part.ts index ebf99edf360..48d0eef2eb4 100644 --- a/lib/part.ts +++ b/lib/part.ts @@ -1,4 +1,6 @@ import { Point } from './point' +import { Path } from './path' +import { Attributes } from './attributes' export class Part { id: string; @@ -6,12 +8,14 @@ export class Part { points: { [index: string]: Point; } + paths: { [index: string]: Path; } = {}; + attributes = new Attributes(); [propName: string]: any; constructor(id: string) { this.id = id; this.render = (id.substr(0,1) === '_') ? false : true; - this.points = {}; + this.points = {'origin': new Point(0,0)}; return this; } diff --git a/lib/path.ts b/lib/path.ts new file mode 100644 index 00000000000..cd01d67af24 --- /dev/null +++ b/lib/path.ts @@ -0,0 +1,78 @@ +import { Point } from './point' +import { Attributes } from './attributes' + + +export class Path { + constructor() { + return this; + } + + ops: { + type: "move" | "line" | "curve" | "close"; + to?: Point; + cp1?: Point; + cp2?: Point; + }[] = []; + attributes: Attributes = new Attributes(); + + /** Adds a move operation to Point to */ + move(to: Point): Path { + this.ops.push({type: "move", to}); + + return this; + } + + /** Adds a line operation to Point to */ + line(to: Point): Path { + this.ops.push({type: "line", to}); + + return this; + } + + /** Adds a line operation to Point to */ + curve(cp1: Point, cp2: Point, to: Point): Path { + this.ops.push({type: "curve", cp1, cp2, to}); + + return this; + } + + /** Adds a close operation */ + close(): Path { + this.ops.push({type: "close"}); + + return this; + } + + /** Returns SVG pathstring for this path */ + asPathstring() { + let d = ''; + for(let op of this.ops) { + switch (op.type) { + case 'move': + d += `M ${op.to!.x},${op.to!.y}`; + break; + case 'line': + d += ` L ${op.to!.x},${op.to!.y}`; + break; + case 'curve': + d += ` C ${op.cp1!.x},${op.cp1!.y} ${op.cp2!.x},${op.cp2!.y} ${op.to!.x},${op.to!.y}`; + break; + case 'close': + d += ' z'; + break; + default: + throw `${op.type} is not a valid path command`; + break; + } + } + + return d; + } + + /** Returns SVG code for this path */ + render(): string { + this.attributes.add('d', this.asPathstring()); + return ``; + + } +} diff --git a/lib/pattern.ts b/lib/pattern.ts index 9611d68bbee..fdc29a05734 100644 --- a/lib/pattern.ts +++ b/lib/pattern.ts @@ -1,10 +1,13 @@ import { PatternConfig, PatternOption } from './types' import { Part } from './part' +import { Renderer } from './renderer' import { Option } from './option' export class Pattern { config: PatternConfig; - parts: {[propName: string]: Part}; + parts: { + [index: string]: Part; + } options: {[propName: string]: number}; constructor(config: PatternConfig) { @@ -27,4 +30,9 @@ export class Pattern { draft(config: object): void { throw Error('You have to implement the draft() method in your Pattern instance.'); } + + render(pattern: Pattern): string { + let r = new Renderer(); + return r.render(pattern); + } } diff --git a/lib/point.ts b/lib/point.ts index 7ed8d6683c0..008bbd6cb8e 100644 --- a/lib/point.ts +++ b/lib/point.ts @@ -1,12 +1,12 @@ import { round, rad2deg, deg2rad } from './utils'; - const PRECISION = 2; -// A poor man's rad2deg() + export class Point { x: number; y: number; + constructor(x: number, y: number) { this.x = round(x); this.y = round(y); diff --git a/lib/renderer.ts b/lib/renderer.ts new file mode 100644 index 00000000000..26e1519e5ad --- /dev/null +++ b/lib/renderer.ts @@ -0,0 +1,96 @@ +import { Part } from './part' +import { Path } from './path' +import { Pattern } from './pattern' +import { Attributes } from './attributes' + +export class Renderer { + svg = ''; + readonly TAB = ' '; + tabs: number = 0; + freeId: number = 1; + openGroups: string[] = []; + + constructor() { + return this; + } + + /** Renders a draft object as SVG */ + render(pattern: Pattern, final: boolean = false): string { + let svg = this.openGroup('draftContainer'); + for (let partId in pattern.parts) { + let part = pattern.parts[partId]; + if (part.render) { + svg += this.openGroup(part.id, part.attributes); + svg += this.renderPart(part); + svg += this.closeGroup(); + } + } + svg += this.closeGroup(); + + return svg; + } + + /** Returns SVG code for a Part object */ + renderPart(part: Part): string { + let svg = ''; + for (let pathId in part.paths) { + let path = part.paths[pathId]; + if(path.render) svg += this.renderPath(path); + } + // includes + // text on path + // notes + // dimensions + // texts + // snippets + + return svg; + } + + /** Returns SVG code for a Path object */ + renderPath(path: Path): string { + return `${this.nl()}`; + } + + + /** Returns SVG code to open a group */ + protected openGroup(id: string, attributes?: Attributes): string { + let svg = this.nl()+this.nl(); + svg += ``; + svg += this.nl(); + svg += ``; + this.indent(); + this.openGroups.push(id); + + return svg; + } + + /** Returns SVG code to close a group */ + protected closeGroup(): string { + this.outdent(); + + return `${this.nl()}${this.nl()}`; + } + + /** Returns a linebreak + identation */ + protected nl(): string { + return "\n"+this.TAB; + } + + /** Increases indentation by 1 */ + protected indent(): void { + this.tabs += 1; + } + + /** Decreases indentation by 1 */ + protected outdent(): void { + this.tabs -= 1; + } + + /** Returns an unused ID */ + protected getUid() { + this.freeId += 1; + + return this.freeId; + } +} diff --git a/lib/utils.ts b/lib/utils.ts index adb0b5f9222..5c26a696aa2 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,3 +1,5 @@ +import { Point } from './point' + /** Rounds a value to PRECISION */ export function round(value: number): number { return Math.round(value * 1e2) / 1e2; @@ -13,3 +15,48 @@ export function deg2rad(degrees: number): number { return degrees / 57.29577951308232; } +/** Find intersection of two (endless) lines */ +export function beamsCross(a1: Point, a2: Point, b1: Point, b2: Point): Point | false { + let slopeA = a1.slope(a2); + let slopeB = b1.slope(b2); + if(slopeA === slopeB) return false; // Parallel lines + + if(a1.x === a2.x) return new Point(a1.x, slopeB * a1.x + (b1.y - (slopeB * b1.x))); // Vertical line A + else if(b1.x === b2.x) return new Point(b1.x, slopeA * b1.x + (a1.y - (slopeA * a1.x))); // Vertical line B + else { + // Swap points if line A or B goes from right to left + if(a1.x > a2.x) { + let tmp = a1.copy(); + a1 = a2.copy(); + a2 = tmp; + } + if(b1.x > b2.x) { + let tmp = b1.copy(); + b1 = b2.copy(); + b2 = tmp; + } + // Find y intercept + let iA = a1.y - (slopeA * a1.x); + let iB = b1.y - (slopeB * b1.x); + + // Find intersection + let x = (iB - iA) / (slopeA - slopeB); + let y = slopeA * x *iA; + + return new Point(x, y); + } +} + +/** Find intersection of two line segments */ +export function linesCross(a1: Point, a2: Point, b1: Point, b2: Point): Point | false { + let p = beamsCross(a1,a2,b1,b2); + if(p) { + let lenA = a1.dist(a2); + let lenB = b1.dist(b2); + let lenC = a1.dist(p) + p.dist(a2); + let lenD = b1.dist(p) + p.dist(b2); + if (round(lenA) == round(lenC) && round(lenB) == round(lenD)) return p; + } + return false; +} + diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 00000000000..36f480185d9 --- /dev/null +++ b/tests/utils.test.js @@ -0,0 +1,77 @@ +var expect = require('chai').expect; +var Point = require('../dist/lib/point').Point; +var utils = require('../dist/lib/utils'); + +it('should round a value', () => { + expect(utils.round(1.2345)).to.equal(1.23); + expect(utils.round(-9.876)).to.equal(-9.88); + expect(utils.round(12)).to.equal(12); +}); + +it('should convert radians to degrees', () => { + expect(utils.rad2deg(Math.PI/2)).to.equal(90); + expect(utils.rad2deg(Math.PI)).to.equal(180); + expect(utils.rad2deg(Math.PI*2)).to.equal(360); +}); + +it('should convert degrees to radians', () => { + expect(utils.deg2rad(90)).to.equal(Math.PI/2); + expect(utils.deg2rad(180)).to.equal(Math.PI); + expect(utils.deg2rad(360)).to.equal(Math.PI*2); +}); + +it('should return a line intersection', () => { + let a = new Point(-10,-10); + let b = new Point(10,-10); + let c = new Point(10,10); + let d = new Point(-10,10); + let result = utils.beamsCross(a,c,b,d); + expect(result.x).to.equal(0); + expect(result.y).to.equal(0); + expect(utils.beamsCross(a,b,c,d)).to.be.false; + + // Debug + let e = new Point(0,0); + let f = new Point(25,25); + let g = new Point(0,60); + let h = new Point(25,35); + // Should be 30,30, but is 30,0 + console.log(utils.beamsCross(e,f,g,h)); + //$p->newPoint(2,25,25); + //$p->newPoint(3,0,60); + //$p->newPoint(4,25,35); + //$this->assertFalse($p->linesCross(1,2,3,4)); + //$this->assertFalse($p->beamsCross(1,3,2,4)); + // + //$expect = new \Freesewing\Point(); + //$expect->setX(30); + //$expect->setY(30); + //$this->assertEquals($p->beamsCross(1,2,3,4), $expect); + // + //$p->newPoint(4,10,-10); + //$expect->setX(7.5); + //$expect->setY(7.5); + //$this->assertEquals($p->linesCross(1,2,3,4), $expect); + // +}); + +it('should return a line segment intersection', () => { + let a = new Point(-10,-10); + let b = new Point(10,-10); + let c = new Point(10,10); + let d = new Point(-10,10); + let result = utils.linesCross(a,c,b,d); + expect(result.x).to.equal(0); + expect(result.y).to.equal(0); + expect(utils.linesCross(a,b,c,d)).to.be.false; + let e = new Point(5,-5); + expect(utils.linesCross(a,b,c,e)).to.be.false; + + let f = new Point(1,10); + let g = new Point(0,49); + let h = new Point(-20,40); + let i = new Point(20,40); + console.log(utils.beamsCross(g,f,h,i)); + +}); +