Skip to content

Commit

Permalink
add Polygon v1
Browse files Browse the repository at this point in the history
  • Loading branch information
ericyd committed Jan 9, 2024
1 parent 43b1a6d commit 65be859
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 15 deletions.
1 change: 1 addition & 0 deletions lib/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './path.js'
export * from './defs.js'
export * from './line-segment.js'
export * from './rectangle.js'
export * from './polygon.js'
66 changes: 66 additions & 0 deletions lib/components/polygon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Vector2 } from '../vector2.js'
import { Rectangle } from './rectangle.js'
import { Tag } from './tag.js'

/**
* @typedef {object} PolygonAttributes
* @property {Vector2[]} points
*/

export class Polygon extends Tag {
/**
* Initialize to "empty" rectangle
* @type {Rectangle}
*/
#boundingBox = new Rectangle({x: 0, y: 0, width: 0, height: 0})
/**
* @param {PolygonAttributes} attributes
*/
constructor(attributes = { points: [] }) {
if (!Array.isArray(attributes.points) || attributes.points.length === 0) {
// Maybe this will change in the future if I make a builder
throw new Error('Cannot construct a Polygon without points')
}
super('polygon', attributes)
}

/** @returns {Rectangle} */
get boundingBox() {
if (this.#boundingBox.empty()) {
/** @type {Vector2[]} */
// @ts-expect-error JSDoc doesn't allow casting I guess
const points = this.attributes.points
const minX = Math.min(...points.map(({x}) => x))
const maxX = Math.max(...points.map(({x}) => x))
const minY = Math.min(...points.map(({y}) => y))
const maxY = Math.max(...points.map(({y}) => y))
this.#boundingBox = new Rectangle({ x: minX, y: minY, width: maxX - minX, height: maxY - minY})
}
return this.#boundingBox
}

#formatAttributes() {
return Object.entries(this.attributes)
.map(([key, value]) =>
key === 'points'
? // @ts-expect-error TS doesn't know that we're operating on the points property,
// which is of type Vector2[]
`${key}="${value.map((vec) => `${vec.x},${vec.y}`).join(' ')}"`
: `${key}="${value}"`,
)
.join(' ')
}

/**
* @returns {string}
*/
render() {
// unfortunately requires it's own render method because of the `points` format.
// This probably deserves a more nuanced treatment by the Tag class, we'll see.
return [
`<${this.tagName} ${this.#formatAttributes()}>`,
this.children.map((child) => child.render()).join(''),
`</${this.tagName}>`,
].join('')
}
}
27 changes: 27 additions & 0 deletions lib/components/polygon.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, it } from 'node:test'
import { Polygon } from './polygon.js'
import assert from 'node:assert'
import { vec2 } from '../vector2.js'

describe('Polygon', () => {
describe('render', () => {
it('formats `points` correctly', () => {
const poly = new Polygon({ points: [vec2(0, 0), vec2(100, 100)] })
const actual = poly.render()
assert.strictEqual(actual, '<polygon points="0,0 100,100"></polygon>')
})

it('includes other properties', () => {
const poly = new Polygon({
points: [vec2(0, 0), vec2(100, 100)],
fill: '#000',
stroke: '#432',
})
const actual = poly.render()
assert.strictEqual(
actual,
'<polygon points="0,0 100,100" fill="#000" stroke="#432"></polygon>',
)
})
})
})
7 changes: 7 additions & 0 deletions lib/components/rectangle.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ export class Rectangle extends Tag {
height,
})
}

/**
* @returns {boolean}
*/
empty() {
return this.x === 0 && this.y === 0 && this.width === 0 && this.height === 0
}
}

/**
Expand Down
40 changes: 25 additions & 15 deletions lib/components/svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Path, path } from './path.js'
import { Rectangle, rect } from './rectangle.js'
import { LineSegment } from './line-segment.js'
import { ColorRgb } from '../color/rgb.js'
import { Polygon } from './polygon.js'

/**
* @typedef {object} SvgAttributes
Expand Down Expand Up @@ -62,12 +63,12 @@ export class Svg extends Tag {

// TODO: find a more generic way of expressing this "instance or builder" pattern
/**
* @param {Path | ((path: Path) => void)} pathOrBuilder
* @param {Path | ((path: Path) => void)} instanceOrBuilder
*/
path(pathOrBuilder) {
return pathOrBuilder instanceof Path
? this.addChild(pathOrBuilder)
: this.addChild(path(pathOrBuilder))
path(instanceOrBuilder) {
return instanceOrBuilder instanceof Path
? this.addChild(instanceOrBuilder)
: this.addChild(path(instanceOrBuilder))
}

/**
Expand All @@ -80,21 +81,30 @@ export class Svg extends Tag {
/**
* TODO: "or builder" can be anything accepted by the "circle" helper
* TODO: add overload type defs
* @param {Circle | ((circle: Circle) => void)} circleOrBuilder
* @param {Circle | ((circle: Circle) => void)} instanceOrBuilder
*/
circle(circleOrBuilder) {
return circleOrBuilder instanceof Circle
? this.addChild(circleOrBuilder)
: this.addChild(circle(circleOrBuilder))
circle(instanceOrBuilder) {
return instanceOrBuilder instanceof Circle
? this.addChild(instanceOrBuilder)
: this.addChild(circle(instanceOrBuilder))
}

/**
* @param {Rectangle | ((rect: Rectangle) => void)} rectOrBuilder
* @param {Rectangle | ((rect: Rectangle) => void)} instanceOrBuilder
*/
rect(rectOrBuilder) {
return rectOrBuilder instanceof Rectangle
? this.addChild(rectOrBuilder)
: this.addChild(rect(rectOrBuilder))
rect(instanceOrBuilder) {
return instanceOrBuilder instanceof Rectangle
? this.addChild(instanceOrBuilder)
: this.addChild(rect(instanceOrBuilder))
}

/**
* @param {Polygon | import('./polygon.js').PolygonAttributes} instanceOrBuilder
*/
polygon(instanceOrBuilder) {
return instanceOrBuilder instanceof Polygon
? this.addChild(instanceOrBuilder)
: this.addChild(new Polygon(instanceOrBuilder))
}

/**
Expand Down

0 comments on commit 65be859

Please sign in to comment.