diff --git a/docs/API.md b/docs/API.md index ba128d8..1ef3551 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,5 +1,5 @@ -# Affineplane API Documentation v2.17.1 +# Affineplane API Documentation v2.18.0 Welcome to affineplane API reference documentation. These docs are generated with [yamdog](https://axelpale.github.io/yamdog/). @@ -166,6 +166,7 @@ and thus can be represented in any basis without loss of information. - [affineplane.box2.getBasisInverse](#affineplanebox2getbasisinverse) - [affineplane.box2.getBounds](#affineplanebox2getbounds) - [affineplane.box2.getCircle](#affineplanebox2getcircle) +- [affineplane.box2.getInnerSquare](#affineplanebox2getinnersquare) - [affineplane.box2.getMinimumBounds](#affineplanebox2getminimumbounds) - [affineplane.box2.getPath](#affineplanebox2getpath) - [affineplane.box2.getPoints](#affineplanebox2getpoints) @@ -489,6 +490,24 @@ Aliases: [affineplane.box2.getSphere](#affineplanebox2getsphere) Source: [getCircle.js](https://github.com/axelpale/affineplane/blob/main/lib/box2/getCircle.js) + +## [affineplane](#affineplane).[box2](#affineplanebox2).[getInnerSquare](#affineplanebox2getinnersquare)(box) + +Get the largest square that fits inside the box and has the same center. + +

Parameters:

+ +- *box* + - a [box2](#affineplanebox2), in the reference basis. + + +

Returns:

+ +- a [box2](#affineplanebox2), in the reference basis. + + +Source: [getInnerSquare.js](https://github.com/axelpale/affineplane/blob/main/lib/box2/getInnerSquare.js) + ## [affineplane](#affineplane).[box2](#affineplanebox2).[getMinimumBounds](#affineplanebox2getminimumbounds)(boxes) @@ -1506,6 +1525,7 @@ Represented with an object `{ x, y, z, r }` for the origin and the radius. - [affineplane.circle3.area](#affineplanecircle3area) - [affineplane.circle3.atCenter](#affineplanecircle3atcenter) - [affineplane.circle3.boundingBox](#affineplanecircle3boundingbox) +- [affineplane.circle3.boundingCircle](#affineplanecircle3boundingcircle) - [affineplane.circle3.collide](#affineplanecircle3collide) - [affineplane.circle3.collideCircle](#affineplanecircle3collidecircle) - [affineplane.circle3.collideSegment](#affineplanecircle3collidesegment) @@ -1618,6 +1638,28 @@ Get outer cuboid boundary of the given circle. Source: [boundingBox.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/boundingBox.js) + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[boundingCircle](#affineplanecircle3boundingcircle)(circles) + +Find a circle that encloses all the given circles +when projected onto the same xy-plane. +The resulting circle shares the largest z coordinate of the given circles. +The result is approximate but is quaranteed to contain the optimal +(projected) bounding circle. + +

Parameters:

+ +- *circles* + - an array of [circle3](#affineplanecircle3) + + +

Returns:

+ +- a [circle3](#affineplanecircle3) + + +Source: [boundingCircle.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/boundingCircle.js) + ## [affineplane](#affineplane).[circle3](#affineplanecircle3).[collide](#affineplanecircle3collide)(c, cc) @@ -10963,20 +11005,6 @@ The zero vector in 2D Source: [vec2/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/index.js) - -## [affineplane](#affineplane).[vec3](#affineplanevec3).[ZERO](#affineplanevec3zero) - -The zero vector in 3D - -Source: [vec3/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec3/index.js) - - -## [affineplane](#affineplane).[vec4](#affineplanevec4).[ZERO](#affineplanevec4zero) - -The zero vector in 4D - -Source: [vec4/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec4/index.js) - ## [affineplane](#affineplane).[vec2](#affineplanevec2).[add](#affineplanevec2add)(v, w) @@ -11612,46 +11640,6 @@ Translation of the plane does not affect the vector. Source: [transitTo.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/transitTo.js) - -## [affineplane](#affineplane).[vec2](#affineplanevec2).[unit](#affineplanevec2unit)(v) - -Get unit vector parallel to the given vector. -The magnitude of unit vector is equal to one. -If zero vector is given, assume direction towards positive x. - -

Parameters:

- -- *v* - - a [vec2](#affineplanevec2) - - -

Returns:

- -- a [vec2](#affineplanevec2), magnitude of one. - - -Aliases: [affineplane.vec2.normalize](#affineplanevec2normalize) - -Source: [unit.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/unit.js) - - -## [affineplane](#affineplane).[vec2](#affineplanevec2).[validate](#affineplanevec2validate)(v) - -Check if object is a valid [vec2](#affineplanevec2). - -

Parameters:

- -- *v* - - an object - - -

Returns:

- -- a boolean - - -Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/validate.js) - ## [affineplane](#affineplane).[vec3](#affineplanevec3) @@ -11707,6 +11695,60 @@ and rotation when represented on different plane. Source: [vec3/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec3/index.js) + +## [affineplane](#affineplane).[vec3](#affineplanevec3).[ZERO](#affineplanevec3zero) + +The zero vector in 3D + +Source: [vec3/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec3/index.js) + + +## [affineplane](#affineplane).[vec4](#affineplanevec4).[ZERO](#affineplanevec4zero) + +The zero vector in 4D + +Source: [vec4/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec4/index.js) + + +## [affineplane](#affineplane).[vec2](#affineplanevec2).[unit](#affineplanevec2unit)(v) + +Get unit vector parallel to the given vector. +The magnitude of unit vector is equal to one. +If zero vector is given, assume direction towards positive x. + +

Parameters:

+ +- *v* + - a [vec2](#affineplanevec2) + + +

Returns:

+ +- a [vec2](#affineplanevec2), magnitude of one. + + +Aliases: [affineplane.vec2.normalize](#affineplanevec2normalize) + +Source: [unit.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/unit.js) + + +## [affineplane](#affineplane).[vec2](#affineplanevec2).[validate](#affineplanevec2validate)(v) + +Check if object is a valid [vec2](#affineplanevec2). + +

Parameters:

+ +- *v* + - an object + + +

Returns:

+ +- a boolean + + +Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/validate.js) + ## [affineplane](#affineplane).[vec3](#affineplanevec3).[add](#affineplanevec3add)(v, w) diff --git a/lib/box2/getInnerSquare.js b/lib/box2/getInnerSquare.js new file mode 100644 index 0000000..cea3ee4 --- /dev/null +++ b/lib/box2/getInnerSquare.js @@ -0,0 +1,36 @@ +module.exports = (box) => { + // @affineplane.box2.getInnerSquare(box) + // + // Get the largest square that fits inside the box and has the same center. + // + // Parameters + // box + // a box2, in the reference basis. + // + // Return + // a box2, in the reference basis. + // + + if (box.h < box.w) { + const offset = (box.w - box.h) / 2 + return { + a: box.a, + b: box.b, + x: box.x + offset * box.a, // offset along width + y: box.y + offset * box.b, + w: box.h, // square + h: box.h + } + } + // else box.h >= box.w + + const offset = (box.h - box.w) / 2 + return { + a: box.a, + b: box.b, + x: box.x - offset * box.b, // offset along height; orient. rotated 90 deg + y: box.y + offset * box.a, + w: box.w, + h: box.w // square + } +} diff --git a/lib/box2/index.js b/lib/box2/index.js index e2f769b..672da6b 100644 --- a/lib/box2/index.js +++ b/lib/box2/index.js @@ -36,6 +36,7 @@ exports.getBasisInverse = require('./getBasisInverse') exports.getBounds = require('./getBounds') exports.getCircle = require('./getCircle') exports.getSphere = exports.getCircle +exports.getInnerSquare = require('./getInnerSquare') exports.getMinimumBounds = require('./getMinimumBounds') exports.getPath = require('./getPath') exports.getPoints = exports.getPath diff --git a/lib/circle3/boundingCircle.js b/lib/circle3/boundingCircle.js new file mode 100644 index 0000000..1fac66f --- /dev/null +++ b/lib/circle3/boundingCircle.js @@ -0,0 +1,66 @@ +module.exports = (circles) => { + // @affineplane.circle3.boundingCircle(circles) + // + // Find a circle that encloses all the given circles + // when projected onto the same xy-plane. + // The resulting circle shares the largest z coordinate of the given circles. + // The result is approximate but is quaranteed to contain the optimal + // (projected) bounding circle. + // + // Parameters + // circles + // an array of circle3 + // + // Return + // a circle3 + // + + const n = circles.length + if (n === 0) { + throw new Error('Cannot compute bounding circle for empty set of circles.') + } + + // Find bounding box + const c0 = circles[0] + let minx = c0.x - c0.r + let maxx = c0.x + c0.r + let miny = c0.y - c0.r + let maxy = c0.y + c0.r + let maxz = c0.z + for (let i = 1; i < n; i += 1) { + const c = circles[i] + const mix = c.x - c.r + const max = c.x + c.r + const miy = c.y - c.r + const may = c.y + c.r + const maz = c.z + if (mix < minx) { minx = mix } + if (max > maxx) { maxx = max } + if (miy < miny) { miny = miy } + if (may > maxy) { maxy = may } + if (maz > maxz) { maxz = maz } + } + + // TODO Find better bounding box + + // Find the center of the bounding box. + const ox = minx + (maxx - minx) / 2 + const oy = miny + (maxy - miny) / 2 + + // Find max radius + let maxr = 0 + for (let i = 0; i < n; i += 1) { + const c = circles[i] + const dx = c.x - ox + const dy = c.y - oy + const d = Math.sqrt(dx * dx + dy * dy) + c.r + if (maxr < d) { maxr = d } + } + + return { + x: ox, + y: oy, + z: maxz, + r: maxr + } +} diff --git a/lib/circle3/index.js b/lib/circle3/index.js index 715ce64..33ab499 100644 --- a/lib/circle3/index.js +++ b/lib/circle3/index.js @@ -21,6 +21,7 @@ exports.almostEqual = require('./almostEqual') exports.area = require('./area') exports.atCenter = require('./atCenter') exports.boundingBox = require('./boundingBox') +exports.boundingCircle = require('./boundingCircle') exports.collide = require('./collide') exports.collideCircle = exports.collide exports.collideSegment = require('./collideSegment') diff --git a/lib/helm3/transitFrom.js b/lib/helm3/transitFrom.js index 05b9fc7..46112f3 100644 --- a/lib/helm3/transitFrom.js +++ b/lib/helm3/transitFrom.js @@ -1,4 +1,3 @@ - module.exports = (tr, source) => { // @affineplane.helm3.transitFrom(tr, source) // diff --git a/lib/point2/transitFrom.js b/lib/point2/transitFrom.js index 9ca0d96..a63515f 100644 --- a/lib/point2/transitFrom.js +++ b/lib/point2/transitFrom.js @@ -1,4 +1,3 @@ - module.exports = (point, source) => { // @affineplane.point2.transitFrom(point, source) // diff --git a/lib/vec2/transitFrom.js b/lib/vec2/transitFrom.js index 89dc745..2565a1a 100644 --- a/lib/vec2/transitFrom.js +++ b/lib/vec2/transitFrom.js @@ -1,4 +1,3 @@ - module.exports = (vec, plane) => { // @affineplane.vec2.transitFrom(vec, plane) // diff --git a/lib/version.js b/lib/version.js index e541e5c..0a37470 100644 --- a/lib/version.js +++ b/lib/version.js @@ -1,2 +1,2 @@ // Generated by genversion. -module.exports = '2.17.1' +module.exports = '2.18.0' diff --git a/package.json b/package.json index 0e5df71..918ef62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "affineplane", - "version": "2.17.1", + "version": "2.18.0", "description": "Affine geometry library for 2D and 3D spaces", "keywords": [ "affine", @@ -38,12 +38,12 @@ }, "license": "MIT", "devDependencies": { - "async": "^3.2.4", + "async": "^3.2.5", "genversion": "^3.1.1", - "nodemon": "^2.0.21", - "standard": "^17.0.0", - "tap-arc": "^0.3.5", - "tape": "^5.6.3", + "nodemon": "^3.0.2", + "standard": "^17.1.0", + "tap-arc": "^1.2.2", + "tape": "^5.7.2", "yamdog": "^2.1.0" }, "scripts": { diff --git a/test/box2/getInnerSquare.test.js b/test/box2/getInnerSquare.test.js new file mode 100644 index 0000000..e53fc95 --- /dev/null +++ b/test/box2/getInnerSquare.test.js @@ -0,0 +1,35 @@ +const affineplane = require('../../index') +const box2 = affineplane.box2 + +module.exports = (ts) => { + ts.test('case: basic inner square', (t) => { + const z = { a: 1, b: 0, x: 0, y: 0, w: 0, h: 0 } + t.deepEqual(box2.getInnerSquare(z), z, 'should be empty') + + const zw = { a: 1, b: 0, x: 0, y: 0, w: 10, h: 0 } + t.deepEqual( + box2.getInnerSquare(zw), + { a: 1, b: 0, x: 5, y: 0, w: 0, h: 0 }, + 'should be empty and at the middle' + ) + + const r = { a: 1, b: 0, x: 0, y: 0, w: 10, h: 10 } + t.deepEqual(box2.getInnerSquare(r), r, 'should be itself') + + const rr = { a: 1, b: 0, x: 200, y: 200, w: 10, h: 6 } + t.deepEqual( + box2.getInnerSquare(rr), + { a: 1, b: 0, x: 202, y: 200, w: 6, h: 6 }, + 'should be square at the middle' + ) + + const rrr = { a: 0, b: 1, x: 200, y: 200, w: 10, h: 6 } + t.deepEqual( + box2.getInnerSquare(rrr), + { a: 0, b: 1, x: 200, y: 202, w: 6, h: 6 }, + 'should handle rotation and preserve box orientation' + ) + + t.end() + }) +} diff --git a/test/box2/index.test.js b/test/box2/index.test.js index 53e9708..aac1f75 100644 --- a/test/box2/index.test.js +++ b/test/box2/index.test.js @@ -13,6 +13,7 @@ const units = { getBasisInverse: require('./getBasisInverse.test'), getBounds: require('./getBounds.test'), getCircle: require('./getCircle.test'), + getInnerSquare: require('./getInnerSquare.test'), getMinimumBounds: require('./getMinimumBounds.test'), getPath: require('./getPath.test'), getSegments: require('./getSegments.test'), diff --git a/test/circle3/boundaries.test.js b/test/circle3/boundaries.test.js index 96bbc32..ac050cd 100644 --- a/test/circle3/boundaries.test.js +++ b/test/circle3/boundaries.test.js @@ -16,4 +16,29 @@ module.exports = (ts) => { t.end() }) + + ts.test('case: basic bounding circle', (t) => { + t.deepEqual( + circle3.boundingCircle([{ x: 0, y: 0, z: 0, r: 0 }]), + { x: 0, y: 0, z: 0, r: 0 }, + 'should be zero circle' + ) + + t.deepEqual( + circle3.boundingCircle([{ x: 0, y: 0, z: 0, r: 1 }]), + { x: 0, y: 0, z: 0, r: 1 }, + 'bounding circle of single circle should be self' + ) + + t.deepEqual( + circle3.boundingCircle([ + { x: -1, y: 0, z: 0, r: 1 }, + { x: 1, y: 0, z: 1, r: 1 } + ]), + { x: 0, y: 0, z: 1, r: 2 }, + 'should be perfect bounding circle on largest z' + ) + + t.end() + }) }