diff --git a/README.md b/README.md index c105ffe6..968cc2f4 100644 --- a/README.md +++ b/README.md @@ -24,24 +24,35 @@ Shapes with position in space: - [box2](https://axelpale.github.io/affineplane/docs/API.html#affineplanebox2), a cuboid in 2D, `{a,b,x,y,w,h}` - [box3](https://axelpale.github.io/affineplane/docs/API.html#affineplanebox3), a cuboid in 3D, `{a,b,x,y,z,w,h,d}` -- [dir2](https://axelpale.github.io/affineplane/docs/API.html#affineplanedir2), direction in 2D, `{x,y}` of unit length -- [dir3](https://axelpale.github.io/affineplane/docs/API.html#affineplanedir3), direction in 3D, `{x,y,z}` of unit length -- [dist2](https://axelpale.github.io/affineplane/docs/API.html#affineplanedist2), distance between two points in 2D, `number` -- [dist3](https://axelpale.github.io/affineplane/docs/API.html#affineplanedist3), distance between two points in 3D, `number` +- [dir2](https://axelpale.github.io/affineplane/docs/API.html#affineplanedir2), a direction in 2D, `{x,y}` of unit length +- [dir3](https://axelpale.github.io/affineplane/docs/API.html#affineplanedir3), a direction in 3D, `{x,y,z}` of unit length +- [dist2](https://axelpale.github.io/affineplane/docs/API.html#affineplanedist2), a distance between two points in 2D, `number` +- [dist3](https://axelpale.github.io/affineplane/docs/API.html#affineplanedist3), a distance between two points in 3D, `number` - [line2](https://axelpale.github.io/affineplane/docs/API.html#affineplaneline2), a line in 2D, `{origin,span}` - [line3](https://axelpale.github.io/affineplane/docs/API.html#affineplaneline3), a line in 3D, `{origin,span}` +- [orient2](https://axelpale.github.io/affineplane/docs/API.html#affineplaneorient2), an orientation in 2D, `{a,b}` +- [path2](https://axelpale.github.io/affineplane/docs/API.html#affineplanepath2), an sequence of points in 2D, `[{x,y},...]` +- [path3](https://axelpale.github.io/affineplane/docs/API.html#affineplanepath3), an sequence of points in 3D, `[{x,y,z},...]` - [plane2](https://axelpale.github.io/affineplane/docs/API.html#affineplaneplane2), a plane in 2D, `{a,b,x,y}` - [plane3](https://axelpale.github.io/affineplane/docs/API.html#affineplaneplane3), an xy-plane in 3D, `{a,b,x,y,z}` -- [point2](https://axelpale.github.io/affineplane/docs/API.html#affineplanepoint2), a location on a plane, `{x,y}` -- [point3](https://axelpale.github.io/affineplane/docs/API.html#affineplanepoint3), a location in a 3D space, `{x,y,z}` -- [size2](https://axelpale.github.io/affineplane/docs/API.html#affineplanesize2), a rectangle size on a plane, `{w,h}` +- [point2](https://axelpale.github.io/affineplane/docs/API.html#affineplanepoint2), a location in 2D, `{x,y}` +- [point3](https://axelpale.github.io/affineplane/docs/API.html#affineplanepoint3), a location in 3D, `{x,y,z}` +- [scalar1](https://axelpale.github.io/affineplane/docs/API.html#affineplanescalar1), a first-order measure (length), `s` +- [scalar2](https://axelpale.github.io/affineplane/docs/API.html#affineplanescalar2), a second-order measure (area), `ss` +- [scalar3](https://axelpale.github.io/affineplane/docs/API.html#affineplanescalar3), a third-order measure (volume), `s` +- [segment2](https://axelpale.github.io/affineplane/docs/API.html#affineplanesegment2), a line segment in 2D space, `[{x,y},{x,y}]` +- [size2](https://axelpale.github.io/affineplane/docs/API.html#affineplanesize2), a rectangle size in 2D, `{w,h}` +- [size3](https://axelpale.github.io/affineplane/docs/API.html#affineplanesize3), a cuboid size in 3D, `{w,h,d}` +- [sphere2](https://axelpale.github.io/affineplane/docs/API.html#affineplanesphere2), a circle in 2D, `{x,y,r}` +- [sphere3](https://axelpale.github.io/affineplane/docs/API.html#affineplanesphere3), a sphere in 3D, `{x,y,z,r}` Movements of shapes: - [helm2](https://axelpale.github.io/affineplane/docs/API.html#affineplanehelm2), a [helmert](https://en.wikipedia.org/wiki/Helmert_transformation) transformation in 2D, `{a,b,x,y}` - [helm3](https://axelpale.github.io/affineplane/docs/API.html#affineplanehelm3), a [helmert](https://en.wikipedia.org/wiki/Helmert_transformation) in 2D with 3D translation, `{a,b,x,y,z}` - [vec2](https://axelpale.github.io/affineplane/docs/API.html#affineplanevector2), a vector in 2D, `{x,y}` -- [vec3](https://axelpale.github.io/affineplane/docs/API.html#affineplanevector2), a vector in 3D, `{x,y,z}` +- [vec3](https://axelpale.github.io/affineplane/docs/API.html#affineplanevector3), a vector in 3D, `{x,y,z}` +- [vec4](https://axelpale.github.io/affineplane/docs/API.html#affineplanevector3), a vector in 4D, `{x,y,z,w}` See [API docs](https://axelpale.github.io/affineplane/docs/API.html) for more. diff --git a/docs/API.md b/docs/API.md index a2860ec0..16810a47 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,5 +1,5 @@ -# Affineplane API Documentation v2.13.0 +# Affineplane API Documentation v2.14.0 Welcome to affineplane API reference documentation. These docs are generated with [yamdog](https://axelpale.github.io/yamdog/). @@ -19,6 +19,8 @@ The functions are grouped in the following submodules. - [affineplane.angle](#affineplaneangle) - [affineplane.box2](#affineplanebox2) - [affineplane.box3](#affineplanebox3) +- [affineplane.circle2](#affineplanecircle2) +- [affineplane.circle3](#affineplanecircle3) - [affineplane.dir2](#affineplanedir2) - [affineplane.dir3](#affineplanedir3) - [affineplane.dist2](#affineplanedist2) @@ -44,6 +46,7 @@ The functions are grouped in the following submodules. - [affineplane.scalar2](#affineplanescalar2) - [affineplane.scalar3](#affineplanescalar3) - [affineplane.segment2](#affineplanesegment2) +- [affineplane.segment3](#affineplanesegment3) - [affineplane.size2](#affineplanesize2) - [affineplane.size3](#affineplanesize3) - [affineplane.sphere2](#affineplanesphere2) @@ -1244,9 +1247,11 @@ Project a 3D box onto a target plane. If a camera is given, project perspectively. Otherwise, project orthogonally along z axis. The resulting box is in 2D. + We only project the front face of the 3D box. This is because if we projected a full 3D box perspectively, -we would get a lattice mesh which we are not currently interested in. +we would get a lattice mesh in which we are not currently interested. +To scale the box towards camera, see [affineplane.box3.homothety](#affineplanebox3homothety).

Parameters:

@@ -1460,6 +1465,477 @@ represent size. Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/box3/validate.js) + +## [affineplane](#affineplane).[circle2](#affineplanecircle2) + +Alias of [affineplane.sphere2](#affineplanesphere2) + +Source: [sphere2/index.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere2/index.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3) + +Flat round circle in three dimensional space. Parallel to the xy-plane. + +Represented with an object `{ x, y, z, r }` for the origin and the radius. + + +

Contents:

+ + +- [affineplane.circle3.almostEqual](#affineplanecircle3almostequal) +- [affineplane.circle3.area](#affineplanecircle3area) +- [affineplane.circle3.atCenter](#affineplanecircle3atcenter) +- [affineplane.circle3.boundingBox](#affineplanecircle3boundingbox) +- [affineplane.circle3.collide](#affineplanecircle3collide) +- [affineplane.circle3.collideCircle](#affineplanecircle3collidecircle) +- [affineplane.circle3.collideSegment](#affineplanecircle3collidesegment) +- [affineplane.circle3.copy](#affineplanecircle3copy) +- [affineplane.circle3.create](#affineplanecircle3create) +- [affineplane.circle3.hasPoint](#affineplanecircle3haspoint) +- [affineplane.circle3.homothety](#affineplanecircle3homothety) +- [affineplane.circle3.offset](#affineplanecircle3offset) +- [affineplane.circle3.polarOffset](#affineplanecircle3polaroffset) +- [affineplane.circle3.projectTo](#affineplanecircle3projectto) +- [affineplane.circle3.projectToPlane](#affineplanecircle3projecttoplane) +- [affineplane.circle3.rotateBy](#affineplanecircle3rotateby) +- [affineplane.circle3.scaleBy](#affineplanecircle3scaleby) +- [affineplane.circle3.size](#affineplanecircle3size) +- [affineplane.circle3.transitFrom](#affineplanecircle3transitfrom) +- [affineplane.circle3.transitTo](#affineplanecircle3transitto) +- [affineplane.circle3.translate](#affineplanecircle3translate) +- [affineplane.circle3.validate](#affineplanecircle3validate) + + +Source: [circle3/index.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/index.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[almostEqual](#affineplanecircle3almostequal)(c, d[, tolerance]) + +Test if two circles are almost equal by the margin of tolerance. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *d* + - a [circle3](#affineplanecircle3) +- *tolerance* + - optional number, default to [affineplane.epsilon](#affineplaneepsilon). Set to 0 for strict comparison. + + +

Returns:

+ +- a boolean + + +Source: [almostEqual.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/almostEqual.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[area](#affineplanecircle3area)(c) + +Get area of the circle. + +

Parameters:

+ +- a [circle3](#affineplanecircle3) + + +

Returns:

+ +- a [scalar2](#affineplanescalar2), a number representing area + + +Source: [area.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/area.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[atCenter](#affineplanecircle3atcenter)(c) + +Get the center point of the circle. +Note that the [circle3](#affineplanecircle3) object itself can act as a [point3](#affineplanepoint3) in many cases. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) + + +

Returns:

+ +- a [point3](#affineplanepoint3) + + +Source: [atCenter.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/atCenter.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[boundingBox](#affineplanecircle3boundingbox)(circle) + +Get outer cuboid boundary of the given circle. + +

Parameters:

+ +- *circle* + - a [circle3](#affineplanecircle3), in the reference basis. + + +

Returns:

+ +- a [box3](#affineplanebox3), in the reference basis. + + +Source: [boundingBox.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/boundingBox.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[collide](#affineplanecircle3collide)(c, cc) + +Detect collision between two circles. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *cc* + - a [circle3](#affineplanecircle3) + + +

Returns:

+ +- boolean, true if the circles collide + + +Aliases: [affineplane.circle3.collideCircle](#affineplanecircle3collidecircle) + +Source: [collide.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/collide.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[collideCircle](#affineplanecircle3collidecircle)(c, cc) + +Alias of [affineplane.circle3.collide](#affineplanecircle3collide) + +Source: [collide.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/collide.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[collideSegment](#affineplanecircle3collidesegment)(c, seg) + +Detect collision between a circle and a line segment. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *seg* + - a [segment3](#affineplanesegment3) + + +

Returns:

+ +- boolean, true if the shapes collide + + +Source: [collideSegment.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/collideSegment.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[copy](#affineplanecircle3copy)(c) + +Copy a circle object. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) + + +

Returns:

+ +- a [circle3](#affineplanecircle3) + + +Source: [copy.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/copy.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[create](#affineplanecircle3create)(x, y, z, r) + +Create a circle object in 3D. The circle is a flat round shape +parallel to xy-plane. + +

Parameters:

+ +- *x* + - a number +- *y* + - a number +- *z* + - a number +- *r* + - a number, the radius + + +

Returns:

+ +- a [circle3](#affineplanecircle3) + + +Source: [create.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/create.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[hasPoint](#affineplanecircle3haspoint)(c, point) + +Detect collision between a circle and a point. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *point* + - a [point2](#affineplanepoint2) + + +

Returns:

+ +- boolean, true if the point is at the edge or inside the circle. + + +Source: [hasPoint.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/hasPoint.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[homothety](#affineplanecircle3homothety)(circle, origin, ratio) + +Perform homothety for the circle about a pivot. +In other words, scale the circle by the given ratio, +so that the origin point stays fixed. + +

Parameters:

+ +- *circle* + - a [circle3](#affineplanecircle3) +- *origin* + - a [point3](#affineplanepoint3), the transform origin, the pivot point +- *ratio* + - a number, the scaling ratio + + +

Returns:

+ +- a [circle3](#affineplanecircle3) + + +Aliases: [affineplane.circle3.scaleBy](#affineplanecircle3scaleby) + +Source: [homothety.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/homothety.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[offset](#affineplanecircle3offset)(c, dx, dy[, dz]) + +Offset a circle by scalars dx, dy, dz. +See [affineplane.circle3.translate](#affineplanecircle3translate) to offset by a vector. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *dx* + - a number, an offset along x-axis. +- *dy* + - a number, an offset along y-axis. +- *dz* + - optional number. The offset along z-axis, default is 0. + + +

Returns:

+ +- a [circle3](#affineplanecircle3), translated + + +Source: [offset.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/offset.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[polarOffset](#affineplanecircle3polaroffset)(circle, distance, theta[, phi]) + +Offset a circle by the given distance towards the direction given by +the spherical theta and phi angles. + +

Parameters:

+ +- *circle* + - a [circle3](#affineplanecircle3) +- *distance* + - a number, the distance from p. +- *theta* + - a number, the angle around z-axis, the azimuthal angle. Clockwise rotation, following the right-hand rule. +- *phi* + - optional number, default π/2. The polar angle in radians measured from the positive z-axis. + + +

Returns:

+ +- a [circle3](#affineplanecircle3) + + +Source: [polarOffset.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/polarOffset.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[projectTo](#affineplanecircle3projectto) + +Alias of [affineplane.circle3.projectToPlane](#affineplanecircle3projecttoplane) + +Source: [projectToPlane.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/projectToPlane.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[projectToPlane](#affineplanecircle3projecttoplane)(circle, plane[, camera]) + +Project a circle onto a plane in 3D space. The result is a 2D circle. +If the camera is undefined, project orthogonally. + +

Parameters:

+ +- *sphere* + - a [circle3](#affineplanecircle3) in the reference space. +- *plane* + - a [plane3](#affineplaneplane3) in the reference space. The target plane. +- *camera* + - optional [point3](#affineplanepoint3) in the reference space. The camera position. + + +

Returns:

+ +- a [circle2](#affineplanecircle2) on the target plane. + + +Aliases: [affineplane.circle3.projectTo](#affineplanecircle3projectto) + +Source: [projectToPlane.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/projectToPlane.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[rotateBy](#affineplanecircle3rotateby)(c, origin, radians) + +Rotate a circle about a line parallel to z-axis that goes through +the given origin point. The rotation direction follows the right hand rule +about the positive z-axis. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *origin* + - a [point3](#affineplanepoint3), the point that defines the line around which to rotate +- *radians* + - a number, angle in radians + + +

Returns:

+ +- a [circle3](#affineplanecircle3), the rotated circle + + +Source: [rotateBy.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/rotateBy.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[scaleBy](#affineplanecircle3scaleby) + +Alias of [affineplane.circle3.homothety](#affineplanecircle3homothety) + +Source: [homothety.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/homothety.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[size](#affineplanecircle3size)(c) + +Get the cuboid size of a circle. Circles always have zero depth. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) in the reference basis. + + +

Returns:

+ +- a [size3](#affineplanesize3) in the reference basis. + + +Source: [size.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/size.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[transitFrom](#affineplanecircle3transitfrom)(circle, source) + +Transit a [circle3](#affineplanecircle3) from the source basis +to the reference basis. + +

Parameters:

+ +- *circle* + - a [circle3](#affineplanecircle3) in the source basis. +- *source* + - a [plane3](#affineplaneplane3), the source basis, represented in the reference basis. + + +

Returns:

+ +- a [circle3](#affineplanecircle3), represented in the reference basis. + + +Source: [transitFrom.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/transitFrom.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[transitTo](#affineplanecircle3transitto)(circle, target) + +Transit a [circle3](#affineplanecircle3) to the target basis +from the reference basis. + +

Parameters:

+ +- *circle* + - a [circle3](#affineplanecircle3) in the source basis. +- *source* + - a [plane3](#affineplaneplane3), the source basis, represented in the reference basis. + + +

Returns:

+ +- a [circle3](#affineplanecircle3), represented in the reference basis. + + +Source: [transitTo.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/transitTo.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[translate](#affineplanecircle3translate)(c, vec) + +Translate a circle by the vector. Does not affect radius. +See [affineplane.circle3.offset](#affineplanecircle3offset) to translate by scalars. + +

Parameters:

+ +- *c* + - a [circle3](#affineplanecircle3) +- *vec* + - a [vec3](#affineplanevec3) + + +

Returns:

+ +- a [circle3](#affineplanecircle3), translated + + +Source: [translate.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/translate.js) + + +## [affineplane](#affineplane).[circle3](#affineplanecircle3).[validate](#affineplanecircle3validate)(c) + +Check if the object is a valid [circle3](#affineplanecircle3). +A valid [circle3](#affineplanecircle3) has x, y, z, r properties that are valid numbers. + +

Parameters:

+ +- *c* + - an object + + +

Returns:

+ +- a boolean, true if valid + + +Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/circle3/validate.js) + ## [affineplane](#affineplane).[dir2](#affineplanedir2) @@ -2346,6 +2822,7 @@ See [affineplane.plane2](#affineplaneplane2) for a positional variant. - [affineplane.helm2.limitDilation](#affineplanehelm2limitdilation) - [affineplane.helm2.multiply](#affineplanehelm2multiply) - [affineplane.helm2.projectTo](#affineplanehelm2projectto) +- [affineplane.helm2.projectToCameraTransform](#affineplanehelm2projecttocameratransform) - [affineplane.helm2.projectToPlane](#affineplanehelm2projecttoplane) - [affineplane.helm2.rotateBy](#affineplanehelm2rotateby) - [affineplane.helm2.scaleBy](#affineplanehelm2scaleby) @@ -2846,6 +3323,31 @@ Alias of [affineplane.helm2.projectToPlane](#affineplanehelm2projecttoplane) Source: [projectToPlane.js](https://github.com/axelpale/affineplane/blob/main/lib/helm2/projectToPlane.js) + +## [affineplane](#affineplane).[helm2](#affineplanehelm2).[projectToCameraTransform](#affineplanehelm2projecttocameratransform)(helm, origin, camera) + +Convert the dilation in the transform to a translation of the camera, +so that the plane would dilate the same amount due to the perspective. +This can be used to convert pinch gestures to forward movement. +Also invert rotation and translation for camera movement. + +

Parameters:

+ +- *helm* + - a [helm2](#affineplanehelm2) in the reference basis. Applied at origin. +- *origin* + - a [point3](#affineplanepoint3) in the reference basis. Position of the transform. +- *camera* + - a [point3](#affineplanepoint3), in the reference basis. + + +

Returns:

+ +- a [helm3](#affineplanehelm3), with scale of 1, in the reference basis. + + +Source: [projectToCameraTransform.js](https://github.com/axelpale/affineplane/blob/main/lib/helm2/projectToCameraTransform.js) + ## [affineplane](#affineplane).[helm2](#affineplanehelm2).[projectToPlane](#affineplanehelm2projecttoplane)(tr, plane[, camera]) @@ -3226,6 +3728,7 @@ See [affineplane.plane3](#affineplaneplane3) for a positional variant. - [affineplane.helm3.create](#affineplanehelm3create) - [affineplane.helm3.det](#affineplanehelm3det) - [affineplane.helm3.determinant](#affineplanehelm3determinant) +- [affineplane.helm3.difference](#affineplanehelm3difference) - [affineplane.helm3.equal](#affineplanehelm3equal) - [affineplane.helm3.equals](#affineplanehelm3equals) - [affineplane.helm3.fromArray](#affineplanehelm3fromarray) @@ -3471,6 +3974,28 @@ Alias of [affineplane.helm3.det](#affineplanehelm3det) Source: [det.js](https://github.com/axelpale/affineplane/blob/main/lib/helm3/det.js) + +## [affineplane](#affineplane).[helm3](#affineplanehelm3).[difference](#affineplanehelm3difference)(h, hh) + +Compute a transformation that maps the codomain of hh to the codomain of h. +In other words, find transformation T such that T*hh = h <=> T = h*inv(hh). +The result is the difference between the transformations h and hh. + +

Parameters:

+ +- *h* + - a [helm3](#affineplanehelm3) +- *hh* + - a [helm3](#affineplanehelm3) + + +

Returns:

+ +- a [helm3](#affineplanehelm3) + + +Source: [difference.js](https://github.com/axelpale/affineplane/blob/main/lib/helm3/difference.js) + ## [affineplane](#affineplane).[helm3](#affineplanehelm3).[equal](#affineplanehelm3equal)(tr, ts) @@ -4696,6 +5221,7 @@ is located +20 units along x-axis of the reference plane. - [affineplane.plane2.getScale](#affineplaneplane2getscale) - [affineplane.plane2.invert](#affineplaneplane2invert) - [affineplane.plane2.limitScale](#affineplaneplane2limitscale) +- [affineplane.plane2.orientation](#affineplaneplane2orientation) - [affineplane.plane2.projectTo](#affineplaneplane2projectto) - [affineplane.plane2.projectToPlane](#affineplaneplane2projecttoplane) - [affineplane.plane2.rotateBy](#affineplaneplane2rotateby) @@ -4830,17 +5356,33 @@ Create a [plane2](#affineplaneplane2) from an origin point and a basis vector. - a [vec2](#affineplanevec2) on the reference plane. This vector is the basis vector for the x-axis of the plane. The basis vector for y can be found 90deg clockwise from x-axis. -

Returns:

+

Returns:

+ +- a [plane2](#affineplaneplane2) + + +Source: [create.js](https://github.com/axelpale/affineplane/blob/main/lib/plane2/create.js) + + +## [affineplane](#affineplane).[plane2](#affineplaneplane2).[difference](#affineplaneplane2difference)(p, pp) + +Find the difference between two planes. In other words, +compute such transformation T that maps the plane pp to the plane p. + +

Parameters:

+ +- *p* + - a [plane2](#affineplaneplane2), in the reference basis. +- *pp* + - a [plane2](#affineplaneplane2), in the reference basis. -- a [plane2](#affineplaneplane2) +

Returns:

-Source: [create.js](https://github.com/axelpale/affineplane/blob/main/lib/plane2/create.js) +- a [helm2](#affineplanehelm2), in the reference basis. - -## [affineplane](#affineplane).[plane2](#affineplaneplane2).[difference](#affineplaneplane2difference)(source, target) -Alias of [affineplane.plane2.between](#affineplaneplane2between) +Aliases: [affineplane.plane2.between](#affineplaneplane2between) Source: [difference.js](https://github.com/axelpale/affineplane/blob/main/lib/plane2/difference.js) @@ -4975,6 +5517,25 @@ min and max (inclusive). Source: [limitScale.js](https://github.com/axelpale/affineplane/blob/main/lib/plane2/limitScale.js) + +## [affineplane](#affineplane).[plane2](#affineplaneplane2).[orientation](#affineplaneplane2orientation)(plane) + +The orientation of the plane, i.e. the rotation from default. +If the plane is singular, falls back to the default orientation. + +

Parameters:

+ +- *plane* + - a [plane2](#affineplaneplane2), in the reference basis + + +

Returns:

+ +- a [orient2](#affineplaneorient2), in the reference basis + + +Source: [orientation.js](https://github.com/axelpale/affineplane/blob/main/lib/plane2/orientation.js) + ## [affineplane](#affineplane).[plane2](#affineplaneplane2).[projectTo](#affineplaneplane2projectto) @@ -5323,6 +5884,8 @@ relative to its reference plane. - [affineplane.plane3.getScale](#affineplaneplane3getscale) - [affineplane.plane3.invert](#affineplaneplane3invert) - [affineplane.plane3.limitScale](#affineplaneplane3limitscale) +- [affineplane.plane3.orientation](#affineplaneplane3orientation) +- [affineplane.plane3.projectByDepth](#affineplaneplane3projectbydepth) - [affineplane.plane3.projectTo](#affineplaneplane3projectto) - [affineplane.plane3.projectToDepth](#affineplaneplane3projecttodepth) - [affineplane.plane3.projectToPlane](#affineplaneplane3projecttoplane) @@ -5456,9 +6019,29 @@ Create a plane from 3D origin point and 2D basis vector. Source: [create.js](https://github.com/axelpale/affineplane/blob/main/lib/plane3/create.js) -## [affineplane](#affineplane).[plane3](#affineplaneplane3).[difference](#affineplaneplane3difference)(source, target) +## [affineplane](#affineplane).[plane3](#affineplaneplane3).[difference](#affineplaneplane3difference)(p, pp) + +Find the difference between the two planes. +In other words, compute a transformation that would map the plane pp +to the plane p: `T * pp = p <=> T = p * inv(pp)` + +To represent planes on each other, see [affineplane.plane3.transitFrom](#affineplaneplane3transitfrom) +and [affineplane.plane3.transitTo](#affineplaneplane3transitto). + +

Parameters:

+ +- *p* + - a [plane3](#affineplaneplane3), in the reference basis. +- *pp* + - a [plane3](#affineplaneplane3), in the reference basis. + + +

Returns:

+ +- a [helm3](#affineplanehelm3), a transformation, in the reference basis. -Alias of [affineplane.plane3.between](#affineplaneplane3between) + +Aliases: [affineplane.plane3.between](#affineplaneplane3between) Source: [difference.js](https://github.com/axelpale/affineplane/blob/main/lib/plane3/difference.js) @@ -5608,6 +6191,53 @@ min and max (inclusive). Source: [limitScale.js](https://github.com/axelpale/affineplane/blob/main/lib/plane3/limitScale.js) + +## [affineplane](#affineplane).[plane3](#affineplaneplane3).[orientation](#affineplaneplane3orientation)(plane) + +The orientation of the plane, i.e. the rotation from default. +If the plane is singular, falls back to the default orientation. + +

Parameters:

+ +- *plane* + - a [plane3](#affineplaneplane3), in the reference basis + + +

Returns:

+ +- a [orient2](#affineplaneorient2), in the reference basis + + +Source: [orientation.js](https://github.com/axelpale/affineplane/blob/main/lib/plane3/orientation.js) + + +## [affineplane](#affineplane).[plane3](#affineplaneplane3).[projectByDepth](#affineplaneplane3projectbydepth)(plane, origin, deltaDepth) + +Project a plane so that it translates by +the given delta depth from the origin. +The plane is also scaled so that it would look +similar from the origin point of view. + +If the origin point is on the plane, +not projection is made and the given plane is returned as-is. + +

Parameters:

+ +- *plane* + - a [plane3](#affineplaneplane3) in the reference basis. +- *origin* + - a [point3](#affineplanepoint3) in the reference basis. +- *deltaDepth* + - a number, can be negative. + + +

Returns:

+ +- a [plane3](#affineplaneplane3) in the reference basis. + + +Source: [projectByDepth.js](https://github.com/axelpale/affineplane/blob/main/lib/plane3/projectByDepth.js) + ## [affineplane](#affineplane).[plane3](#affineplaneplane3).[projectTo](#affineplaneplane3projectto) @@ -5996,6 +6626,7 @@ An affine space does not have origin; `{ x:0, y:0 }` is not an origin. - [affineplane.point2.move](#affineplanepoint2move) - [affineplane.point2.offset](#affineplanepoint2offset) - [affineplane.point2.polarOffset](#affineplanepoint2polaroffset) +- [affineplane.point2.projectByDistance](#affineplanepoint2projectbydistance) - [affineplane.point2.projectTo](#affineplanepoint2projectto) - [affineplane.point2.projectToLine](#affineplanepoint2projecttoline) - [affineplane.point2.projectToPlane](#affineplanepoint2projecttoplane) @@ -6295,6 +6926,30 @@ Create a point away from p at the given distance and angle. Source: [polarOffset.js](https://github.com/axelpale/affineplane/blob/main/lib/point2/polarOffset.js) + +## [affineplane](#affineplane).[point2](#affineplanepoint2).[projectByDistance](#affineplanepoint2projectbydistance)(point, origin, distance) + +Perform homothety about the origin point so that the point translates +by the given distance. If the point and origin are the same point, +no translation will occur and the original point is returned. + +

Parameters:

+ +- *point* + - a [point2](#affineplanepoint2) +- *origin* + - a [point2](#affineplanepoint2), the pivot point +- *distance* + - a number, can be negative. + + +

Returns:

+ +- a [point2](#affineplanepoint2) + + +Source: [projectByDistance.js](https://github.com/axelpale/affineplane/blob/main/lib/point2/projectByDistance.js) + ## [affineplane](#affineplane).[point2](#affineplanepoint2).[projectTo](#affineplanepoint2projectto) @@ -6540,6 +7195,7 @@ translation of the plane on which they are represented. - [affineplane.point3.difference](#affineplanepoint3difference) - [affineplane.point3.direction](#affineplanepoint3direction) - [affineplane.point3.distance](#affineplanepoint3distance) +- [affineplane.point3.distanceToPlane](#affineplanepoint3distancetoplane) - [affineplane.point3.equal](#affineplanepoint3equal) - [affineplane.point3.equals](#affineplanepoint3equals) - [affineplane.point3.fromArray](#affineplanepoint3fromarray) @@ -6547,6 +7203,7 @@ translation of the plane on which they are represented. - [affineplane.point3.mean](#affineplanepoint3mean) - [affineplane.point3.offset](#affineplanepoint3offset) - [affineplane.point3.polarOffset](#affineplanepoint3polaroffset) +- [affineplane.point3.projectByDistance](#affineplanepoint3projectbydistance) - [affineplane.point3.projectTo](#affineplanepoint3projectto) - [affineplane.point3.projectToPlane](#affineplanepoint3projecttoplane) - [affineplane.point3.rotateAroundLine](#affineplanepoint3rotatearoundline) @@ -6705,6 +7362,26 @@ Euclidean distance between two points. Source: [distance.js](https://github.com/axelpale/affineplane/blob/main/lib/point3/distance.js) + +## [affineplane](#affineplane).[point3](#affineplanepoint3).[distanceToPlane](#affineplanepoint3distancetoplane)(p, plane) + +Euclidean distance between point and plane. + +

Parameters:

+ +- *p* + - a [point3](#affineplanepoint3) +- *plane* + - a [plane3](#affineplaneplane3) + + +

Returns:

+ +- a number, a [scalar1](#affineplanescalar1), a [dist3](#affineplanedist3), a distance + + +Source: [distanceToPlane.js](https://github.com/axelpale/affineplane/blob/main/lib/point3/distanceToPlane.js) + ## [affineplane](#affineplane).[point3](#affineplanepoint3).[equal](#affineplanepoint3equal)(p, q) @@ -6830,6 +7507,30 @@ and pitch angle. Source: [polarOffset.js](https://github.com/axelpale/affineplane/blob/main/lib/point3/polarOffset.js) + +## [affineplane](#affineplane).[point3](#affineplanepoint3).[projectByDistance](#affineplanepoint3projectbydistance)(point, origin, distance) + +Perform homothety about the origin point so that the point translates +by the given distance. If the point and origin are the same point, +no translation will occur and the original point is returned. + +

Parameters:

+ +- *point* + - a [point3](#affineplanepoint3) +- *origin* + - a [point3](#affineplanepoint3), the pivot point +- *distance* + - a number, can be negative. + + +

Returns:

+ +- a [point3](#affineplanepoint3) + + +Source: [projectByDistance.js](https://github.com/axelpale/affineplane/blob/main/lib/point3/projectByDistance.js) + ## [affineplane](#affineplane).[point3](#affineplanepoint3).[projectTo](#affineplanepoint3projectto) @@ -7683,6 +8384,7 @@ nor the representation. - [affineplane.scalar1.almostEqual](#affineplanescalar1almostequal) - [affineplane.scalar1.create](#affineplanescalar1create) - [affineplane.scalar1.equal](#affineplanescalar1equal) +- [affineplane.scalar1.projectToPlane](#affineplanescalar1projecttoplane) - [affineplane.scalar1.transitFrom](#affineplanescalar1transitfrom) - [affineplane.scalar1.transitTo](#affineplanescalar1transitto) - [affineplane.scalar1.validate](#affineplanescalar1validate) @@ -7755,6 +8457,30 @@ Test if scalars c, d are strictly equal. Source: [equal.js](https://github.com/axelpale/affineplane/blob/main/lib/scalar1/equal.js) + +## [affineplane](#affineplane).[scalar1](#affineplanescalar1).[projectToPlane](#affineplanescalar1projecttoplane)(scalar, plane[, camera]) + +Project a scalar (e.g. distance) onto another plane in 3d. +If camera is given, project perspectively. +Otherwise, project orthogonally. + +

Parameters:

+ +- *scalar* + - a [scalar1](#affineplanescalar1) on the z=0 plane of the reference space. +- *plane* + - a [plane3](#affineplaneplane3) relative to the reference space. +- *camera* + - optional [point3](#affineplanepoint3) in the reference space. + + +

Returns:

+ +- a [scalar1](#affineplanescalar1), projected on the target plane, and represented in the scale of the target. + + +Source: [projectToPlane.js](https://github.com/axelpale/affineplane/blob/main/lib/scalar1/projectToPlane.js) + ## [affineplane](#affineplane).[scalar1](#affineplanescalar1).[transitFrom](#affineplanescalar1transitfrom)(scalar, source) @@ -7839,6 +8565,7 @@ On the map, an area of 1 squaremeter of ground is represented by - [affineplane.scalar2.almostEqual](#affineplanescalar2almostequal) - [affineplane.scalar2.create](#affineplanescalar2create) - [affineplane.scalar2.equal](#affineplanescalar2equal) +- [affineplane.scalar2.projectToPlane](#affineplanescalar2projecttoplane) - [affineplane.scalar2.transitFrom](#affineplanescalar2transitfrom) - [affineplane.scalar2.transitTo](#affineplanescalar2transitto) - [affineplane.scalar2.validate](#affineplanescalar2validate) @@ -7912,6 +8639,30 @@ Test if scalars c, d are strictly equal. Source: [equal.js](https://github.com/axelpale/affineplane/blob/main/lib/scalar2/equal.js) + +## [affineplane](#affineplane).[scalar2](#affineplanescalar2).[projectToPlane](#affineplanescalar2projecttoplane)(scalar, plane[, camera]) + +Project a second-order scalar (e.g. area) onto another plane in 3D. +If camera is given, project perspectively. +Otherwise, project orthogonally. + +

Parameters:

+ +- *scalar* + - a [scalar2](#affineplanescalar2) on the z=0 plane of the reference space. +- *plane* + - a [plane3](#affineplaneplane3) relative to the reference space. +- *camera* + - optional [point3](#affineplanepoint3) in the reference space. + + +

Returns:

+ +- a [scalar2](#affineplanescalar2), projected on the target plane, and represented in the scale of the target. + + +Source: [projectToPlane.js](https://github.com/axelpale/affineplane/blob/main/lib/scalar2/projectToPlane.js) + ## [affineplane](#affineplane).[scalar2](#affineplanescalar2).[transitFrom](#affineplanescalar2transitfrom)(scalar, source) @@ -8254,6 +9005,126 @@ A valid [segment2](#affineplanesegment2) is an array of two valid [point2](#affi Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/segment2/validate.js) + +## [affineplane](#affineplane).[segment3](#affineplanesegment3) + +Three-dimensional line segment. Represented by the segment start and end +points in an array of length two. + +Example: `[{ x: 0, y: 0, z: 0 }, { x: 1, y: 2, z: 3 }]` + + +

Contents:

+ + +- [affineplane.segment3.create](#affineplanesegment3create) +- [affineplane.segment3.toVector](#affineplanesegment3tovector) +- [affineplane.segment3.transitFrom](#affineplanesegment3transitfrom) +- [affineplane.segment3.transitTo](#affineplanesegment3transitto) +- [affineplane.segment3.validate](#affineplanesegment3validate) + + +Source: [segment3/index.js](https://github.com/axelpale/affineplane/blob/main/lib/segment3/index.js) + + +## [affineplane](#affineplane).[segment3](#affineplanesegment3).[create](#affineplanesegment3create)(p0, p1) + +Create a segment from points. + +

Parameters:

+ +- *p0* + - a [point3](#affineplanepoint3) +- *p1* + - a [point3](#affineplanepoint3) + + +

Returns:

+ +- a [segment3](#affineplanesegment3), an array. + + +Source: [create.js](https://github.com/axelpale/affineplane/blob/main/lib/segment3/create.js) + + +## [affineplane](#affineplane).[segment3](#affineplanesegment3).[toVector](#affineplanesegment3tovector)(seg) + +Convert segment to a vector from the first to the second segment point. + +

Parameters:

+ +- *seg* + - a [segment3](#affineplanesegment3) + + +

Returns:

+ +- a [vec3](#affineplanevec3) + + +Source: [toVector.js](https://github.com/axelpale/affineplane/blob/main/lib/segment3/toVector.js) + + +## [affineplane](#affineplane).[segment3](#affineplanesegment3).[transitFrom](#affineplanesegment3transitfrom)(seg, source) + +Represent a segment in the reference basis. In other words, +transit the segment from the source basis to the reference basis. + +

Parameters:

+ +- *seg* + - a [segment3](#affineplanesegment3), represented in the source basis. +- *source* + - a [plane3](#affineplaneplane3), the source basis, represented in the reference basis. + + +

Returns:

+ +- a [segment3](#affineplanesegment3), represented in the reference basis. + + +Source: [transitFrom.js](https://github.com/axelpale/affineplane/blob/main/lib/segment3/transitFrom.js) + + +## [affineplane](#affineplane).[segment3](#affineplanesegment3).[transitTo](#affineplanesegment3transitto)(seg, target) + +Represent a segment in the target basis. In other words, +transit the segment from the reference basis to the target basis. + +

Parameters:

+ +- *seg* + - a [segment3](#affineplanesegment3), represented in the reference basis. +- *target* + - a [plane3](#affineplaneplane3), the target basis, represented in the reference basis. + + +

Returns:

+ +- a [segment3](#affineplanesegment3), represented in the target basis. + + +Source: [transitTo.js](https://github.com/axelpale/affineplane/blob/main/lib/segment3/transitTo.js) + + +## [affineplane](#affineplane).[segment3](#affineplanesegment3).[validate](#affineplanesegment3validate)(seg) + +Check if the object is a valid [segment3](#affineplanesegment3). +A valid [segment3](#affineplanesegment3) is an array of two valid [point3](#affineplanepoint3) objects. + +

Parameters:

+ +- *seg* + - an object + + +

Returns:

+ +- a boolean, true if valid + + +Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/segment3/validate.js) + ## [affineplane](#affineplane).[size2](#affineplanesize2) @@ -8595,6 +9466,8 @@ Two dimensional sphere, a circle. Represented with an object `{ x, y, r }` for the origin and the radius. +Aliases: [affineplane.circle2](#affineplanecircle2) +

Contents:

@@ -8603,6 +9476,7 @@ Represented with an object `{ x, y, r }` for the origin and the radius. - [affineplane.sphere2.area](#affineplanesphere2area) - [affineplane.sphere2.atCenter](#affineplanesphere2atcenter) - [affineplane.sphere2.boundingBox](#affineplanesphere2boundingbox) +- [affineplane.sphere2.boundingCircle](#affineplanesphere2boundingcircle) - [affineplane.sphere2.collide](#affineplanesphere2collide) - [affineplane.sphere2.copy](#affineplanesphere2copy) - [affineplane.sphere2.create](#affineplanesphere2create) @@ -8697,6 +9571,26 @@ Get outer rectangular boundary of the given sphere. Source: [boundingBox.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere2/boundingBox.js) + +## [affineplane](#affineplane).[sphere2](#affineplanesphere2).[boundingCircle](#affineplanesphere2boundingcircle)(circles) + +Find a circle that encloses all the given circles. +The result is approximate but is quaranteed to contain the optimal +bounding circle. + +

Parameters:

+ +- *circles* + - an array of [circle2](#affineplanecircle2) + + +

Returns:

+ +- a [circle2](#affineplanecircle2) + + +Source: [boundingCircle.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere2/boundingCircle.js) + ## [affineplane](#affineplane).[sphere2](#affineplanesphere2).[collide](#affineplanesphere2collide)(c, cc) @@ -9015,6 +9909,7 @@ Represented with an object `{ x, y, z, r }` for the origin and the radius. - [affineplane.sphere3.area](#affineplanesphere3area) - [affineplane.sphere3.atCenter](#affineplanesphere3atcenter) - [affineplane.sphere3.boundingBox](#affineplanesphere3boundingbox) +- [affineplane.sphere3.boundingSphere](#affineplanesphere3boundingsphere) - [affineplane.sphere3.collide](#affineplanesphere3collide) - [affineplane.sphere3.copy](#affineplanesphere3copy) - [affineplane.sphere3.create](#affineplanesphere3create) @@ -9085,7 +9980,8 @@ Note that the [sphere3](#affineplanesphere3) object itself can act as a [point3]

Parameters:

-- a [sphere3](#affineplanesphere3) +- *sp* + - a [sphere3](#affineplanesphere3)

Returns:

@@ -9113,6 +10009,26 @@ Get outer cuboid boundary of the given sphere. Source: [boundingBox.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere3/boundingBox.js) + +## [affineplane](#affineplane).[sphere3](#affineplanesphere3).[boundingSphere](#affineplanesphere3boundingsphere)(spheres) + +Find a sphere that encloses all the given spheres. +The result is approximate but is quaranteed to contain the optimal +bounding sphere. + +

Parameters:

+ +- *spheres* + - an array of [sphere3](#affineplanesphere3) + + +

Returns:

+ +- a [sphere3](#affineplanesphere3) + + +Source: [boundingSphere.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere3/boundingSphere.js) + ## [affineplane](#affineplane).[sphere3](#affineplanesphere3).[collide](#affineplanesphere3collide)(c, cc) diff --git a/docs/generate.js b/docs/generate.js index 57ac0e33..e432ec33 100644 --- a/docs/generate.js +++ b/docs/generate.js @@ -56,6 +56,8 @@ yamdog.generate({ decor.linkKeywords({ box2: '#affineplanebox2', box3: '#affineplanebox3', + circle2: '#affineplanecircle2', + circle3: '#affineplanecircle3', dir2: '#affineplanedir2', dir3: '#affineplanedir3', dist2: '#affineplanedist2', @@ -80,6 +82,7 @@ yamdog.generate({ scalar2: '#affineplanescalar2', scalar3: '#affineplanescalar3', segment2: '#affineplanesegment2', + segment3: '#affineplanesegment3', size2: '#affineplanesize2', size3: '#affineplanesize3', sphere2: '#affineplanesphere2', diff --git a/lib/box2/index.js b/lib/box2/index.js index b5dc9eb5..a05f6cbc 100644 --- a/lib/box2/index.js +++ b/lib/box2/index.js @@ -9,6 +9,9 @@ // - `x,y` provide origin position in the reference basis. // - `w,h` provide box size on the reference basis. // +exports.UNIT = { a: 1, b: 0, x: -0.5, y: -0.5, w: 1, h: 1 } +exports.ZERO = { a: 1, b: 0, x: 0, y: 0, w: 0, h: 0 } + exports.almostEqual = require('./almostEqual') exports.at = require('./at') exports.atBox = require('./atBox') diff --git a/lib/box3/index.js b/lib/box3/index.js index 6523f42f..1b151cd7 100644 --- a/lib/box3/index.js +++ b/lib/box3/index.js @@ -9,6 +9,9 @@ // - `x,y,z` provide origin position in the reference basis. // - `w,h,d` provide box size on the reference basis. // +exports.UNIT = { a: 1, b: 0, x: -0.5, y: -0.5, z: -0.5, w: 1, h: 1, d: 1 } +exports.ZERO = { a: 1, b: 0, x: 0, y: 0, z: 0, w: 0, h: 0, d: 0 } + exports.almostEqual = require('./almostEqual') exports.at = require('./at') exports.atBox = require('./atBox') diff --git a/lib/box3/projectToPlane.js b/lib/box3/projectToPlane.js index 8019982a..ae5e8706 100644 --- a/lib/box3/projectToPlane.js +++ b/lib/box3/projectToPlane.js @@ -9,9 +9,11 @@ module.exports = (box, target, camera) => { // If a camera is given, project perspectively. // Otherwise, project orthogonally along z axis. // The resulting box is in 2D. + // // We only project the front face of the 3D box. // This is because if we projected a full 3D box perspectively, - // we would get a lattice mesh which we are not currently interested in. + // we would get a lattice mesh in which we are not currently interested. + // To scale the box towards camera, see affineplane.box3.homothety. // // Parameters: // box diff --git a/lib/circle3/almostEqual.js b/lib/circle3/almostEqual.js new file mode 100644 index 00000000..fbdf3f12 --- /dev/null +++ b/lib/circle3/almostEqual.js @@ -0,0 +1,17 @@ +// @affineplane.circle3.almostEqual(c, d[, tolerance]) +// +// Test if two circles are almost equal by the margin of tolerance. +// +// Parameters +// c +// a circle3 +// d +// a circle3 +// tolerance +// optional number, default to affineplane.epsilon. +// .. Set to 0 for strict comparison. +// +// Return +// a boolean +// +module.exports = require('../sphere3/almostEqual') diff --git a/lib/circle3/area.js b/lib/circle3/area.js new file mode 100644 index 00000000..03ec0632 --- /dev/null +++ b/lib/circle3/area.js @@ -0,0 +1,11 @@ +// @affineplane.circle3.area(c) +// +// Get area of the circle. +// +// Parameters: +// a circle3 +// +// Return +// a scalar2, a number representing area +// +module.exports = require('../sphere2/area') diff --git a/lib/circle3/atCenter.js b/lib/circle3/atCenter.js new file mode 100644 index 00000000..b03d90af --- /dev/null +++ b/lib/circle3/atCenter.js @@ -0,0 +1,13 @@ +// @affineplane.circle3.atCenter(c) +// +// Get the center point of the circle. +// Note that the circle3 object itself can act as a point3 in many cases. +// +// Parameters: +// c +// a circle3 +// +// Return +// a point3 +// +module.exports = require('../point3/copy') diff --git a/lib/circle3/boundingBox.js b/lib/circle3/boundingBox.js new file mode 100644 index 00000000..6aa73ba2 --- /dev/null +++ b/lib/circle3/boundingBox.js @@ -0,0 +1,22 @@ +module.exports = (circle) => { + // @affineplane.circle3.boundingBox(circle) + // + // Get outer cuboid boundary of the given circle. + // + // Parameters + // circle + // a circle3, in the reference basis. + // + // Return + // a box3, in the reference basis. + // + + const r = circle.r + const bx = circle.x - r + const by = circle.y - r + const bz = circle.z // flat box + const dia = r + r + + // Construct the bounding rect. + return { a: 1, b: 0, x: bx, y: by, z: bz, w: dia, h: dia, d: 0 } +} diff --git a/lib/circle3/collide.js b/lib/circle3/collide.js new file mode 100644 index 00000000..5adb40c4 --- /dev/null +++ b/lib/circle3/collide.js @@ -0,0 +1,34 @@ +const EPSILON = require('../epsilon') + +module.exports = (c, cc) => { + // @affineplane.circle3.collide(c, cc) + // @affineplane.circle3.collideCircle(c, cc) + // + // Detect collision between two circles. + // + // Parameters: + // c + // a circle3 + // cc + // a circle3 + // + // Return + // boolean, true if the circles collide + // + + // Have separation along z? + const dz = cc.z - c.z + if (Math.abs(dz) > EPSILON) { + return false + } + + // pole distance smaller than radius sum + // sqrt(dx^2 + dy^2) <= limit + // Use <= instead of < because the same origin spheres collide even if r = 0. + const dx = cc.x - c.x + const dy = cc.y - c.y + const limit = cc.r + c.r + + // Replace square root by multiplication. + return dx * dx + dy * dy <= limit * limit +} diff --git a/lib/circle3/collideSegment.js b/lib/circle3/collideSegment.js new file mode 100644 index 00000000..ae345643 --- /dev/null +++ b/lib/circle3/collideSegment.js @@ -0,0 +1,60 @@ +const segToVector = require('../segment3/toVector') +const vecMultiply = require('../vec3/scaleBy') +const pointTranslate = require('../point3/translate') +const EPSILON = require('../epsilon') +const hasPoint = require('./hasPoint') + +module.exports = (c, seg) => { + // @affineplane.circle3.collideSegment(c, seg) + // + // Detect collision between a circle and a line segment. + // + // Parameters: + // c + // a circle3 + // seg + // a segment3 + // + // Return + // boolean, true if the shapes collide + // + + // Does the segment cross the circle plane? + // If both segment ends are on the same side of the plane, + // the segment cannot cross the circle. + const s0 = seg[0] + const s1 = seg[1] + const dz0 = s0.z - c.z + const dz1 = s1.z - c.z + if (dz0 * dz1 > 0) { + return false + } + + // Assert: segment intersects the circle plane. + + const segdz = s1.z - s0.z + if (Math.abs(segdz) < EPSILON) { + // Unsafe to divide. Segment points very close to the circle plane. + // Test one of the points directly. + return hasPoint(c, s0) + } + + // Find intersection point. Because the circle plane is parallel to xy-plane + // we can find the point easily by starting from the first segment point + // and then adding a vector to it, scaled by the normalized dz distance. + const vec = segToVector(seg) + // If z0 at -1 and z1 at +1 and cz at 0, then dz0=-1 and z1-z0=2 + const t = -dz0 / segdz + const svec = vecMultiply(vec, t) + // Find the intersection point. + const inter = pointTranslate(s0, svec) + + // Assert the intersection point is on the circle plane. + + // Test if the intersection point is within the circle. + const dx = inter.x - c.x + const dy = inter.y - c.y + const limit = c.r + // Replace square root with a multiplication: compare squares + return dx * dx + dy * dy <= limit * limit +} diff --git a/lib/circle3/copy.js b/lib/circle3/copy.js new file mode 100644 index 00000000..aaf8a53a --- /dev/null +++ b/lib/circle3/copy.js @@ -0,0 +1,12 @@ +// @affineplane.circle3.copy(c) +// +// Copy a circle object. +// +// Parameters +// c +// a circle3 +// +// Return +// a circle3 +// +module.exports = require('../sphere3/copy') diff --git a/lib/circle3/create.js b/lib/circle3/create.js new file mode 100644 index 00000000..9c17651c --- /dev/null +++ b/lib/circle3/create.js @@ -0,0 +1,21 @@ +module.exports = (x, y, z, r) => { + // @affineplane.circle3.create(x, y, z, r) + // + // Create a circle object in 3D. The circle is a flat round shape + // parallel to xy-plane. + // + // Parameters: + // x + // a number + // y + // a number + // z + // a number + // r + // a number, the radius + // + // Return + // a circle3 + // + return { x, y, z, r } +} diff --git a/lib/circle3/hasPoint.js b/lib/circle3/hasPoint.js new file mode 100644 index 00000000..65393928 --- /dev/null +++ b/lib/circle3/hasPoint.js @@ -0,0 +1,31 @@ +const EPSILON = require('../epsilon') + +module.exports = (c, point) => { + // @affineplane.circle3.hasPoint(c, point) + // + // Detect collision between a circle and a point. + // + // Parameters: + // c + // a circle3 + // point + // a point2 + // + // Return + // boolean, true if the point is at the edge or inside the circle. + // + + const dx = point.x - c.x + const dy = point.y - c.y + const dz = point.z - c.z + + if (Math.abs(dz) <= EPSILON) { + // Close enough along z. + // Test if within radius from the circle center. + const limit = c.r + // Replace a square root by multiplication + return dx * dx + dy * dy <= limit * limit + } + + return false +} diff --git a/lib/circle3/homothety.js b/lib/circle3/homothety.js new file mode 100644 index 00000000..5606e7e4 --- /dev/null +++ b/lib/circle3/homothety.js @@ -0,0 +1,19 @@ +// @affineplane.circle3.homothety(circle, origin, ratio) +// @affineplane.circle3.scaleBy +// +// Perform homothety for the circle about a pivot. +// In other words, scale the circle by the given ratio, +// so that the origin point stays fixed. +// +// Parameters: +// circle +// a circle3 +// origin +// a point3, the transform origin, the pivot point +// ratio +// a number, the scaling ratio +// +// Return: +// a circle3 +// +module.exports = require('../sphere3/homothety') diff --git a/lib/circle3/index.js b/lib/circle3/index.js new file mode 100644 index 00000000..b417bab5 --- /dev/null +++ b/lib/circle3/index.js @@ -0,0 +1,31 @@ +// @affineplane.circle3 +// +// Flat round circle in three dimensional space. Parallel to the xy-plane. +// +// Represented with an object `{ x, y, z, r }` for the origin and the radius. +// +exports.UNIT = { x: 0, y: 0, z: 0, r: 1 } +exports.ZERO = { x: 0, y: 0, z: 0, r: 0 } + +exports.almostEqual = require('./almostEqual') +exports.area = require('./area') +exports.atCenter = require('./atCenter') +exports.boundingBox = require('./boundingBox') +exports.collide = require('./collide') +exports.collideCircle = exports.collide +exports.collideSegment = require('./collideSegment') +exports.copy = require('./copy') +exports.create = require('./create') +exports.hasPoint = require('./hasPoint') +exports.homothety = require('./homothety') +exports.offset = require('./offset') +exports.polarOffset = require('./polarOffset') +exports.projectToPlane = require('./projectToPlane') +exports.projectTo = exports.projectToPlane +exports.rotateBy = require('./rotateBy') +exports.scaleBy = exports.homothety +exports.size = require('./size') +exports.transitFrom = require('./transitFrom') +exports.transitTo = require('./transitTo') +exports.translate = require('./translate') +exports.validate = require('./validate') diff --git a/lib/circle3/offset.js b/lib/circle3/offset.js new file mode 100644 index 00000000..53b0103f --- /dev/null +++ b/lib/circle3/offset.js @@ -0,0 +1,19 @@ +// @affineplane.circle3.offset(c, dx, dy[, dz]) +// +// Offset a circle by scalars dx, dy, dz. +// See affineplane.circle3.translate to offset by a vector. +// +// Parameters: +// c +// a circle3 +// dx +// a number, an offset along x-axis. +// dy +// a number, an offset along y-axis. +// dz +// optional number. The offset along z-axis, default is 0. +// +// Return +// a circle3, translated +// +module.exports = require('../sphere3/offset') diff --git a/lib/circle3/polarOffset.js b/lib/circle3/polarOffset.js new file mode 100644 index 00000000..3235a90e --- /dev/null +++ b/lib/circle3/polarOffset.js @@ -0,0 +1,21 @@ +// @affineplane.circle3.polarOffset(circle, distance, theta[, phi]) +// +// Offset a circle by the given distance towards the direction given by +// the spherical theta and phi angles. +// +// Parameters: +// circle +// a circle3 +// distance +// a number, the distance from p. +// theta +// a number, the angle around z-axis, the azimuthal angle. +// .. Clockwise rotation, following the right-hand rule. +// phi +// optional number, default π/2. The polar angle in radians +// .. measured from the positive z-axis. +// +// Return +// a circle3 +// +module.exports = require('../sphere3/polarOffset') diff --git a/lib/circle3/projectToPlane.js b/lib/circle3/projectToPlane.js new file mode 100644 index 00000000..1711dad6 --- /dev/null +++ b/lib/circle3/projectToPlane.js @@ -0,0 +1,19 @@ +// @affineplane.circle3.projectToPlane(circle, plane[, camera]) +// @affineplane.circle3.projectTo +// +// Project a circle onto a plane in 3D space. The result is a 2D circle. +// If the camera is undefined, project orthogonally. +// +// Parameters: +// sphere +// a circle3 in the reference space. +// plane +// a plane3 in the reference space. The target plane. +// camera +// optional point3 in the reference space. +// .. The camera position. +// +// Return: +// a circle2 on the target plane. +// +module.exports = require('../sphere3/projectToPlane') diff --git a/lib/circle3/rotateBy.js b/lib/circle3/rotateBy.js new file mode 100644 index 00000000..992b806f --- /dev/null +++ b/lib/circle3/rotateBy.js @@ -0,0 +1,18 @@ +// @affineplane.circle3.rotateBy(c, origin, radians) +// +// Rotate a circle about a line parallel to z-axis that goes through +// the given origin point. The rotation direction follows the right hand rule +// about the positive z-axis. +// +// Parameters +// c +// a circle3 +// origin +// a point3, the point that defines the line around which to rotate +// radians +// a number, angle in radians +// +// Return +// a circle3, the rotated circle +// +module.exports = require('../sphere3/rotateBy') diff --git a/lib/circle3/size.js b/lib/circle3/size.js new file mode 100644 index 00000000..cb9f9324 --- /dev/null +++ b/lib/circle3/size.js @@ -0,0 +1,19 @@ +module.exports = (c) => { + // @affineplane.circle3.size(c) + // + // Get the cuboid size of a circle. Circles always have zero depth. + // + // Parameters: + // c + // a circle3 in the reference basis. + // + // Return + // a size3 in the reference basis. + // + const diam = c.r + c.r + return { + w: diam, + h: diam, + d: 0 + } +} diff --git a/lib/circle3/transitFrom.js b/lib/circle3/transitFrom.js new file mode 100644 index 00000000..0e075967 --- /dev/null +++ b/lib/circle3/transitFrom.js @@ -0,0 +1,16 @@ +// @affineplane.circle3.transitFrom(circle, source) +// +// Transit a circle3 from the source basis +// to the reference basis. +// +// Parameters: +// circle +// a circle3 in the source basis. +// source +// a plane3, the source basis, represented +// .. in the reference basis. +// +// Return: +// a circle3, represented in the reference basis. +// +module.exports = require('../sphere3/transitFrom') diff --git a/lib/circle3/transitTo.js b/lib/circle3/transitTo.js new file mode 100644 index 00000000..45c2e7fb --- /dev/null +++ b/lib/circle3/transitTo.js @@ -0,0 +1,16 @@ +// @affineplane.circle3.transitTo(circle, target) +// +// Transit a circle3 to the target basis +// from the reference basis. +// +// Parameters: +// circle +// a circle3 in the source basis. +// source +// a plane3, the source basis, represented +// .. in the reference basis. +// +// Return: +// a circle3, represented in the reference basis. +// +module.exports = require('../sphere3/transitTo') diff --git a/lib/circle3/translate.js b/lib/circle3/translate.js new file mode 100644 index 00000000..dcc0e0fd --- /dev/null +++ b/lib/circle3/translate.js @@ -0,0 +1,15 @@ +// @affineplane.circle3.translate(c, vec) +// +// Translate a circle by the vector. Does not affect radius. +// See affineplane.circle3.offset to translate by scalars. +// +// Parameters: +// c +// a circle3 +// vec +// a vec3 +// +// Return +// a circle3, translated +// +module.exports = require('../sphere3/translate') diff --git a/lib/circle3/validate.js b/lib/circle3/validate.js new file mode 100644 index 00000000..8f2a9d9f --- /dev/null +++ b/lib/circle3/validate.js @@ -0,0 +1,13 @@ +// @affineplane.circle3.validate(c) +// +// Check if the object is a valid circle3. +// A valid circle3 has x, y, z, r properties that are valid numbers. +// +// Parameter +// c +// an object +// +// Return +// a boolean, true if valid +// +module.exports = require('../sphere3/validate') diff --git a/lib/helm2/index.js b/lib/helm2/index.js index 5f996460..268ddec0 100644 --- a/lib/helm2/index.js +++ b/lib/helm2/index.js @@ -60,6 +60,7 @@ exports.multiply = exports.compose exports.inverse = require('./invert') exports.invert = exports.inverse exports.limitDilation = require('./limitDilation') +exports.projectToCameraTransform = require('./projectToCameraTransform') exports.projectToPlane = require('./projectToPlane') exports.projectTo = exports.projectToPlane diff --git a/lib/helm2/projectToCameraTransform.js b/lib/helm2/projectToCameraTransform.js new file mode 100644 index 00000000..2a04dad9 --- /dev/null +++ b/lib/helm2/projectToCameraTransform.js @@ -0,0 +1,92 @@ +const EPSILON = require('../epsilon') + +module.exports = (helm, origin, camera) => { + // @affineplane.helm2.projectToCameraTransform(helm, origin, camera) + // + // Convert the dilation in the transform to a translation of the camera, + // so that the plane would dilate the same amount due to the perspective. + // This can be used to convert pinch gestures to forward movement. + // Also invert rotation and translation for camera movement. + // + // Parameters: + // helm + // a helm2 in the reference basis. Applied at origin. + // origin + // a point3 in the reference basis. Position of the transform. + // camera + // a point3, in the reference basis. + // + // Return: + // a helm3, with scale of 1, in the reference basis. + // + + // How we could transform the camera so that its movement would + // cause the plane transform in the same way and amount as the helmert + // would transform it? + + // Let m be a dilation. + // Let b be a distance measure on the transformation plane. + // Let b0 be a projection of b on the view plane. + // Let b1 be a projection of b on the view plane after the cam translation. + // Let c be the view plane distance from the camera. + // Let z0 be the transformation plane distance from the camera. + // + // Find vector (xv, yv, zv) that would translate the camera so + // that the projection b0 would dilate as much as the transformation + // would scale it. + // + // Therefore: + // (1) m = b1 / b0 + // (2) b / z0 = b0 / c <=> b0 = c * b / z0 + // (3) b / (z0 - zv) = b1 / c <=> b1 = c * b / (z0 - zv) + // + // From (1) we get: + // b1 = m * b0 + // We substitute b1 and b0 according to (2) and (3) so we get: + // c * b / (z0 - zv) = m * c * b / z0 + // Assuming b,c,m != 0: + // m / z0 = 1 / (z0 - zv) + // Which implies: + // zv = z0 * (m - 1) / m + // + const m = Math.sqrt(helm.a * helm.a + helm.b * helm.b) + const z0 = origin.z - camera.z + + if (m < EPSILON) { + // Singular transform that shrinks everything into a point. + // Camera would need to translate infinitely far back. + // Best to keep camera still. + return { a: 1, b: 0, x: 0, y: 0, z: 0 } + } + + const zv = z0 * (m - 1) / m + + // The camera translation happens towards the origin. + // Let x0 and y0 be the origin coordinates w.r.t. the camera. + // Therefore we have xv and yv: + // xv / zv = x0 / z0 and yv / zv = y0 / z0 + // <=> xv = x0 * zv / z0 and yv = y0 * zv / z0 + const x0 = origin.x - camera.x + const y0 = origin.y - camera.y + const rz = zv / z0 + const xv = x0 * rz + const yv = y0 * rz + + // The helm transformation contains also rotation and translation. + // The rotation is inverted because of the camera point of view. + // Rotation matrix inversion is equivalent to its transpose. + const ra = helm.a / m + const rb = -helm.b / m + // Translation is scaled as did the dilation. It is also inverted (=negated) + const dx = -helm.x / m + const dy = -helm.y / m + + // Add the translations before the result. + return { + a: ra, + b: rb, + x: xv + dx, + y: yv + dy, + z: zv + } +} diff --git a/lib/helm3/compose.js b/lib/helm3/compose.js index 7894a9de..ac88d439 100644 --- a/lib/helm3/compose.js +++ b/lib/helm3/compose.js @@ -17,6 +17,8 @@ module.exports = function (tr, ts) { // // For reading aid: + // tr * ts + // = // a -b 0 x s -r 0 i as-br -ar-bs 0 ai-bj+x // b a 0 y * r s 0 j = bs+ar -br+as 0 bi+aj+y // 0 0 n z 0 0 m k 0 0 nm nk+z diff --git a/lib/helm3/difference.js b/lib/helm3/difference.js new file mode 100644 index 00000000..f8bcf2a8 --- /dev/null +++ b/lib/helm3/difference.js @@ -0,0 +1,23 @@ +const compose = require('./compose') +const invert = require('./invert') + +module.exports = (h, hh) => { + // @affineplane.helm3.difference(h, hh) + // + // Compute a transformation that maps the codomain of hh to the codomain of h. + // In other words, find transformation T such that T*hh = h <=> T = h*inv(hh). + // The result is the difference between the transformations h and hh. + // + // Parameters: + // h + // a helm3 + // hh + // a helm3 + // + // Return + // a helm3 + // + + const ihh = invert(hh) + return compose(h, ihh) +} diff --git a/lib/helm3/index.js b/lib/helm3/index.js index 6e5d1c69..1f9938c8 100644 --- a/lib/helm3/index.js +++ b/lib/helm3/index.js @@ -30,6 +30,7 @@ exports.clone = exports.copy exports.create = require('./create') exports.det = require('./det') exports.determinant = exports.det +exports.difference = require('./difference') exports.equal = require('./equal') exports.fromArray = require('./fromArray') exports.fromBasisVector = require('./fromBasisVector') diff --git a/lib/index.js b/lib/index.js index 22cf0180..7d6ef79d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,6 +13,9 @@ exports.basis2z = exports.basis3 exports.box2 = require('./box2') exports.box3 = require('./box3') +exports.circle2 = require('./sphere2') +exports.circle3 = require('./circle3') + // TODO exports.convex2 // TODO exports.convex3 @@ -70,12 +73,12 @@ exports.scalar2 = require('./scalar2') exports.scalar3 = require('./scalar3') exports.segment2 = require('./segment2') -// TODO exports.segment3 = require('./segment3') +exports.segment3 = require('./segment3') exports.size2 = require('./size2') exports.size3 = require('./size3') -exports.sphere2 = require('./sphere2') +exports.sphere2 = exports.circle2 exports.sphere3 = require('./sphere3') exports.vec2 = require('./vec2') diff --git a/lib/plane2/difference.js b/lib/plane2/difference.js index 2455478f..d1f7ebc9 100644 --- a/lib/plane2/difference.js +++ b/lib/plane2/difference.js @@ -1,30 +1,24 @@ const compose = require('./compose') const invert = require('./invert') -module.exports = (source, target) => { - // @affineplane.plane2.difference(source, target) +module.exports = (p, pp) => { + // @affineplane.plane2.difference(p, pp) // @affineplane.plane2.between - /// alt names: relative, delta // - // Represent a source plane on the target plane. - // In other words, find a transition from a source plane A - // to a target plane B from their transitions to - // an intermediate root plane R. - // - // The result is a combination of the inverse of the target plane - // and the source plane. + // Find the difference between two planes. In other words, + // compute such transformation T that maps the plane pp to the plane p. // // Parameters: - // source - // a plane2, the source plane on the reference plane. - // target - // a plane2, the target plane on the reference plane. + // p + // a plane2, in the reference basis. + // pp + // a plane2, in the reference basis. // // Return - // a plane2, the source plane on the target plane. + // a helm2, in the reference basis. // // OPTIMIZE open the functions if projection between is used a lot. - const itarget = invert(target) - return compose(itarget, source) + const ipp = invert(pp) + return compose(p, ipp) } diff --git a/lib/plane2/index.js b/lib/plane2/index.js index 26770de8..6c6c1a7c 100644 --- a/lib/plane2/index.js +++ b/lib/plane2/index.js @@ -42,6 +42,7 @@ exports.getScale = require('./getScale') exports.inverse = require('./invert') exports.invert = exports.inverse exports.limitScale = require('./limitScale') +exports.orientation = require('./orientation') exports.projectToPlane = require('./projectToPlane') exports.projectTo = exports.projectToPlane exports.rotateBy = require('./rotateBy') diff --git a/lib/plane2/orientation.js b/lib/plane2/orientation.js new file mode 100644 index 00000000..cc4aa9a6 --- /dev/null +++ b/lib/plane2/orientation.js @@ -0,0 +1,32 @@ +const EPSILON = require('../epsilon') + +module.exports = (plane) => { + // @affineplane.plane2.orientation(plane) + // + // The orientation of the plane, i.e. the rotation from default. + // If the plane is singular, falls back to the default orientation. + // + // Parameters + // plane + // a plane2, in the reference basis + // + // Return + // a orient2, in the reference basis + // + const ma = plane.a + const mb = plane.b + const m = Math.sqrt(ma * ma + mb * mb) + + if (Math.abs(m) < EPSILON) { + // Fall back to default orientation. + return { + a: 1, + b: 0 + } + } + + return { + a: ma / m, + b: mb / m + } +} diff --git a/lib/plane3/difference.js b/lib/plane3/difference.js index 4d6e9b89..52f20066 100644 --- a/lib/plane3/difference.js +++ b/lib/plane3/difference.js @@ -1,30 +1,28 @@ const compose = require('./compose') const invert = require('./invert') -module.exports = (source, target) => { - // @affineplane.plane3.difference(source, target) +module.exports = (p, pp) => { + // @affineplane.plane3.difference(p, pp) // @affineplane.plane3.between - /// alt names: relative, delta // - // Represent a source plane on the target plane. - // In other words, find a transition from a source plane A - // to a target plane B from their transitions to - // an intermediate root plane R. + // Find the difference between the two planes. + // In other words, compute a transformation that would map the plane pp + // to the plane p: `T * pp = p <=> T = p * inv(pp)` // - // The result is a combination of the inverse of the target plane - // and the source plane. + // To represent planes on each other, see affineplane.plane3.transitFrom + // and affineplane.plane3.transitTo. // // Parameters: - // source - // a plane3, the source plane on the reference plane. - // target - // a plane3, the target plane on the reference plane. + // p + // a plane3, in the reference basis. + // pp + // a plane3, in the reference basis. // // Return - // a plane3, the source plane on the target plane. + // a helm3, a transformation, in the reference basis. // - // OPTIMIZE open the functions if projection between is used a lot. - const itarget = invert(target) - return compose(itarget, source) + // OPTIMIZE open the functions if used a lot. + const ipp = invert(pp) + return compose(p, ipp) } diff --git a/lib/plane3/index.js b/lib/plane3/index.js index 4700aaee..949bf7dd 100644 --- a/lib/plane3/index.js +++ b/lib/plane3/index.js @@ -42,9 +42,11 @@ exports.getScale = require('./getScale') exports.inverse = require('./invert') exports.invert = exports.inverse exports.limitScale = require('./limitScale') +exports.orientation = require('./orientation') +exports.projectByDepth = require('./projectByDepth') +exports.projectToDepth = require('./projectToDepth') exports.projectToPlane = require('./projectToPlane') exports.projectTo = exports.projectToPlane -exports.projectToDepth = require('./projectToDepth') exports.projectToScale = require('./projectToScale') exports.rotateBy = require('./rotateBy') exports.rotateTo = require('./rotateTo') diff --git a/lib/plane3/orientation.js b/lib/plane3/orientation.js new file mode 100644 index 00000000..ef902a9a --- /dev/null +++ b/lib/plane3/orientation.js @@ -0,0 +1,13 @@ +// @affineplane.plane3.orientation(plane) +// +// The orientation of the plane, i.e. the rotation from default. +// If the plane is singular, falls back to the default orientation. +// +// Parameters +// plane +// a plane3, in the reference basis +// +// Return +// a orient2, in the reference basis +// +module.exports = require('../plane2/orientation') diff --git a/lib/plane3/projectByDepth.js b/lib/plane3/projectByDepth.js new file mode 100644 index 00000000..57af0ea9 --- /dev/null +++ b/lib/plane3/projectByDepth.js @@ -0,0 +1,46 @@ +const EPSILON = require('../epsilon') + +module.exports = (plane, origin, deltaDepth) => { + // @affineplane.plane3.projectByDepth(plane, origin, deltaDepth) + // + // Project a plane so that it translates by + // the given delta depth from the origin. + // The plane is also scaled so that it would look + // similar from the origin point of view. + // + // If the origin point is on the plane, + // not projection is made and the given plane is returned as-is. + // + // Parameters: + // plane + // a plane3 in the reference basis. + // origin + // a point3 in the reference basis. + // deltaDepth + // a number, can be negative. + // + // Return: + // a plane3 in the reference basis. + // + + // From origin to plane + const deltaToPlane = plane.z - origin.z + // From origin to the target depth + const deltaToDepth = deltaToPlane + deltaDepth + + if (Math.abs(deltaToPlane) < EPSILON) { + // Origin on the plane. Cannot project by depth. + return plane + } + + // Find scaling ratio. + const m = deltaToDepth / deltaToPlane + + return { + a: m * plane.a, + b: m * plane.b, + x: m * plane.x + (1 - m) * origin.x, + y: m * plane.y + (1 - m) * origin.y, + z: m * plane.z + (1 - m) * origin.z + } +} diff --git a/lib/plane3/transitTo.js b/lib/plane3/transitTo.js index 00302110..8b56d382 100644 --- a/lib/plane3/transitTo.js +++ b/lib/plane3/transitTo.js @@ -1,5 +1,5 @@ const invert = require('./invert') -const transitFrom = require('./transitFrom') +const compose = require('../helm3/compose') module.exports = (plane, target) => { // @affineplane.plane3.transitTo(plane, target) @@ -19,8 +19,12 @@ module.exports = (plane, target) => { // a plane3, represented on the target plane. // - // The plane is a mapping from itself to a target plane. - // We need the projection from the target to the plane. + // The plane is a mapping from itself to the reference: PR + // The target is a mapping from itself to the reference: TR + // To represent plane on the target plane: PT + // We know that PT = inv(TR)*PR + // Therefore invert the target: const itarget = invert(target) - return transitFrom(plane, itarget) + // And multiply inv(TR)*PR + return compose(itarget, plane) } diff --git a/lib/point2/index.js b/lib/point2/index.js index 23cbb4b9..84b92ced 100644 --- a/lib/point2/index.js +++ b/lib/point2/index.js @@ -8,32 +8,12 @@ // ![A point](geometry_point.png) // +exports.ZERO = { x: 0, y: 0 } + // exports.add // Points cannot be added because no origin. // See .translate to add a vector. -exports.almostEqual = require('./almostEqual') -exports.average = require('./average') -exports.mean = exports.average - -exports.copy = require('./copy') - -exports.create = require('./create') - -exports.delta = require('./difference') -exports.diff = exports.delta -exports.difference = exports.delta - -exports.direction = require('./direction') -exports.distance = require('./distance') - -exports.equal = require('./equal') -exports.equals = exports.equal - -exports.fromArray = require('./fromArray') - -exports.homothety = require('./homothety') - // exports.magnitude // Points cannot have magnitude because no origin to measure it. @@ -46,32 +26,38 @@ exports.homothety = require('./homothety') // exports.norm = exports.magnitude // Points cannot have norm because no origin to measure it. -exports.offset = require('./offset') - // exports.opposite // Points cannot have opposite because no origin to oppose to. -exports.polarOffset = require('./polarOffset') - -exports.projectToPlane = require('./projectToPlane') -exports.projectTo = exports.projectToPlane -exports.projectToLine = require('./projectToLine') - -exports.rotateBy = require('./rotateBy') - // exports.rotation // Points cannot have rotation because no origin to rotate about. +exports.almostEqual = require('./almostEqual') +exports.average = require('./average') +exports.copy = require('./copy') +exports.create = require('./create') +exports.delta = require('./difference') +exports.diff = exports.delta +exports.difference = exports.delta +exports.direction = require('./direction') +exports.distance = require('./distance') +exports.equal = require('./equal') +exports.equals = exports.equal +exports.fromArray = require('./fromArray') +exports.homothety = require('./homothety') +exports.offset = require('./offset') +exports.polarOffset = require('./polarOffset') +exports.projectByDistance = require('./projectByDistance') +exports.projectTo = require('./projectToPlane') +exports.projectToLine = require('./projectToLine') +exports.projectToPlane = exports.projectTo +exports.rotateBy = require('./rotateBy') exports.toArray = require('./toArray') - exports.transform = require('./transform') exports.transformMany = require('./transformMany') - exports.transitFrom = require('./transitFrom') exports.transitTo = require('./transitTo') - exports.translate = require('./translate') exports.move = exports.translate - exports.validate = require('./validate') exports.vectorTo = exports.delta diff --git a/lib/point2/projectByDistance.js b/lib/point2/projectByDistance.js new file mode 100644 index 00000000..689243e9 --- /dev/null +++ b/lib/point2/projectByDistance.js @@ -0,0 +1,36 @@ +const EPSILON = require('../epsilon') + +module.exports = (point, origin, distance) => { + // @affineplane.point2.projectByDistance(point, origin, distance) + // + // Perform homothety about the origin point so that the point translates + // by the given distance. If the point and origin are the same point, + // no translation will occur and the original point is returned. + // + // Parameters: + // point + // a point2 + // origin + // a point2, the pivot point + // distance + // a number, can be negative. + // + // Return: + // a point2 + // + const dx = point.x - origin.x + const dy = point.y - origin.y + const d = Math.sqrt(dx * dx + dy * dy) + + if (d < EPSILON) { + // Probably point and origin are the same. + // Cannot know where to project. + return point + } + + const ratio = (d + distance) / d + return { + x: dx * ratio + origin.x, + y: dy * ratio + origin.y + } +} diff --git a/lib/point3/distanceToPlane.js b/lib/point3/distanceToPlane.js new file mode 100644 index 00000000..47030e88 --- /dev/null +++ b/lib/point3/distanceToPlane.js @@ -0,0 +1,19 @@ +module.exports = (p, plane) => { + // @affineplane.point3.distanceToPlane(p, plane) + // + // Euclidean distance between point and plane. + // + // Parameters + // p + // a point3 + // plane + // a plane3 + // + // Return + // a number, a scalar1, a dist3, a distance + // + + // Assert plane3 is parallel to xy-plane + + return Math.abs(plane.z - p.z) +} diff --git a/lib/point3/index.js b/lib/point3/index.js index 2079b605..2bba7109 100644 --- a/lib/point3/index.js +++ b/lib/point3/index.js @@ -5,6 +5,8 @@ // translation of the plane on which they are represented. // +exports.ZERO = { x: 0, y: 0, z: 0 } + exports.almostEqual = require('./almostEqual') exports.average = require('./average') exports.copy = require('./copy') @@ -15,6 +17,7 @@ exports.diff = exports.delta exports.difference = exports.delta exports.direction = require('./direction') exports.distance = require('./distance') +exports.distanceToPlane = require('./distanceToPlane') exports.equal = require('./equal') exports.equals = exports.equal exports.fromArray = require('./fromArray') @@ -22,6 +25,7 @@ exports.homothety = require('./homothety') exports.mean = exports.average exports.offset = require('./offset') exports.polarOffset = require('./polarOffset') +exports.projectByDistance = require('./projectByDistance') exports.projectToPlane = require('./projectToPlane') exports.projectTo = exports.projectToPlane exports.rotateAroundLine = require('./rotateAroundLine') diff --git a/lib/point3/projectByDistance.js b/lib/point3/projectByDistance.js new file mode 100644 index 00000000..011b8f47 --- /dev/null +++ b/lib/point3/projectByDistance.js @@ -0,0 +1,39 @@ +const EPSILON = require('../epsilon') + +module.exports = (point, origin, distance) => { + // @affineplane.point3.projectByDistance(point, origin, distance) + // + // Perform homothety about the origin point so that the point translates + // by the given distance. If the point and origin are the same point, + // no translation will occur and the original point is returned. + // + // Parameters: + // point + // a point3 + // origin + // a point3, the pivot point + // distance + // a number, can be negative. + // + // Return: + // a point3 + // + const dx = point.x - origin.x + const dy = point.y - origin.y + const dz = point.z - origin.z + const d = Math.sqrt(dx * dx + dy * dy + dz * dz) + + if (d < EPSILON) { + // Probably point and origin are the same. + // Cannot know where to project. + return point + } + + const ratio = 1 + distance / d + + return { + x: dx * ratio + origin.x, + y: dy * ratio + origin.y, + z: dz * ratio + origin.z + } +} diff --git a/lib/scalar1/index.js b/lib/scalar1/index.js index 90dd5a7d..bb3d1682 100644 --- a/lib/scalar1/index.js +++ b/lib/scalar1/index.js @@ -11,6 +11,7 @@ exports.almostEqual = require('./almostEqual') exports.create = require('./create') exports.equal = require('./equal') +exports.projectToPlane = require('./projectToPlane') exports.transitFrom = require('./transitFrom') exports.transitTo = require('./transitTo') exports.validate = require('./validate') diff --git a/lib/scalar1/projectToPlane.js b/lib/scalar1/projectToPlane.js new file mode 100644 index 00000000..0a9970f8 --- /dev/null +++ b/lib/scalar1/projectToPlane.js @@ -0,0 +1,41 @@ +const transitTo = require('./transitTo') + +module.exports = (scalar, plane, camera) => { + // @affineplane.scalar1.projectToPlane(scalar, plane[, camera]) + // + // Project a scalar (e.g. distance) onto another plane in 3d. + // If camera is given, project perspectively. + // Otherwise, project orthogonally. + // + // Parameters: + // scalar + // a scalar1 on the z=0 plane of the reference space. + // plane + // a plane3 relative to the reference space. + // camera + // optional point3 in the reference space. + // + // Return + // a scalar1, projected on the target plane, + // .. and represented in the scale of the target. + // + + // If camera is present, we first scale the scalar + // by the amount caused due to perspective. + if (camera) { + // Scaling ratio. + let ratio + // If camera is at the reference depth, + // the ratio would become infinite. + if (camera.z === 0) { + ratio = 0 // instead of infinite + } else { + ratio = (camera.z - plane.z) / camera.z + } + + return transitTo(scalar * ratio, plane) + } + + // Orthogonal projection. Only the plane scale affects. + return transitTo(scalar, plane) +} diff --git a/lib/scalar2/index.js b/lib/scalar2/index.js index 3b770a58..0ed9acbe 100644 --- a/lib/scalar2/index.js +++ b/lib/scalar2/index.js @@ -16,6 +16,7 @@ exports.almostEqual = require('./almostEqual') exports.create = require('./create') exports.equal = require('./equal') +exports.projectToPlane = require('./projectToPlane') exports.transitFrom = require('./transitFrom') exports.transitTo = require('./transitTo') exports.validate = require('./validate') diff --git a/lib/scalar2/projectToPlane.js b/lib/scalar2/projectToPlane.js new file mode 100644 index 00000000..8bbdfaae --- /dev/null +++ b/lib/scalar2/projectToPlane.js @@ -0,0 +1,45 @@ +const transitTo = require('./transitTo') + +module.exports = (scalar, plane, camera) => { + // @affineplane.scalar2.projectToPlane(scalar, plane[, camera]) + // + // Project a second-order scalar (e.g. area) onto another plane in 3D. + // If camera is given, project perspectively. + // Otherwise, project orthogonally. + // + // Parameters: + // scalar + // a scalar2 on the z=0 plane of the reference space. + // plane + // a plane3 relative to the reference space. + // camera + // optional point3 in the reference space. + // + // Return + // a scalar2, projected on the target plane, + // .. and represented in the scale of the target. + // + + // If camera is present, we first scale the scalar + // by the amount caused due to perspective. + if (camera) { + // Scaling ratio. + let ratio + // If camera is at the reference depth, + // the ratio would become infinite. + if (camera.z === 0) { + ratio = 0 // instead of infinite + } else { + ratio = (camera.z - plane.z) / camera.z + } + + // Second order scaling example: when the sides of the square are + // tripled, the area grows 3x3=9 times. + const scaledScalar = scalar * ratio * ratio + + return transitTo(scaledScalar, plane) + } + + // Orthogonal projection. Only the plane scale affects. + return transitTo(scalar, plane) +} diff --git a/lib/segment3/create.js b/lib/segment3/create.js new file mode 100644 index 00000000..713b7baf --- /dev/null +++ b/lib/segment3/create.js @@ -0,0 +1,16 @@ +module.exports = (p0, p1) => { + // @affineplane.segment3.create(p0, p1) + // + // Create a segment from points. + // + // Parameters: + // p0 + // a point3 + // p1 + // a point3 + // + // Return: + // a segment3, an array. + // + return [p0, p1] +} diff --git a/lib/segment3/index.js b/lib/segment3/index.js new file mode 100644 index 00000000..baaf7435 --- /dev/null +++ b/lib/segment3/index.js @@ -0,0 +1,13 @@ +// @affineplane.segment3 +// +// Three-dimensional line segment. Represented by the segment start and end +// points in an array of length two. +// +// Example: `[{ x: 0, y: 0, z: 0 }, { x: 1, y: 2, z: 3 }]` +// + +exports.create = require('./create') +exports.toVector = require('./toVector') +exports.transitFrom = require('./transitFrom') +exports.transitTo = require('./transitTo') +exports.validate = require('./validate') diff --git a/lib/segment3/toVector.js b/lib/segment3/toVector.js new file mode 100644 index 00000000..79a58636 --- /dev/null +++ b/lib/segment3/toVector.js @@ -0,0 +1,18 @@ +module.exports = (seg) => { + // @affineplane.segment3.toVector(seg) + // + // Convert segment to a vector from the first to the second segment point. + // + // Parameter + // seg + // a segment3 + // + // Return + // a vec3 + // + return { + x: seg[1].x - seg[0].x, + y: seg[1].y - seg[0].y, + z: seg[1].z - seg[0].z + } +} diff --git a/lib/segment3/transitFrom.js b/lib/segment3/transitFrom.js new file mode 100644 index 00000000..05988017 --- /dev/null +++ b/lib/segment3/transitFrom.js @@ -0,0 +1,16 @@ +// @affineplane.segment3.transitFrom(seg, source) +// +// Represent a segment in the reference basis. In other words, +// transit the segment from the source basis to the reference basis. +// +// Parameters: +// seg +// a segment3, represented in the source basis. +// source +// a plane3, the source basis, represented +// .. in the reference basis. +// +// Return: +// a segment3, represented in the reference basis. +// +module.exports = require('../path3/transitFrom') diff --git a/lib/segment3/transitTo.js b/lib/segment3/transitTo.js new file mode 100644 index 00000000..56f0d1c6 --- /dev/null +++ b/lib/segment3/transitTo.js @@ -0,0 +1,16 @@ +// @affineplane.segment3.transitTo(seg, target) +// +// Represent a segment in the target basis. In other words, +// transit the segment from the reference basis to the target basis. +// +// Parameters: +// seg +// a segment3, represented in the reference basis. +// target +// a plane3, the target basis, represented +// .. in the reference basis. +// +// Return: +// a segment3, represented in the target basis. +// +module.exports = require('../path3/transitTo') diff --git a/lib/segment3/validate.js b/lib/segment3/validate.js new file mode 100644 index 00000000..4391a8b8 --- /dev/null +++ b/lib/segment3/validate.js @@ -0,0 +1,18 @@ +const validatePoint = require('../point3/validate') + +module.exports = (seg) => { + // @affineplane.segment3.validate(seg) + // + // Check if the object is a valid segment3. + // A valid segment3 is an array of two valid point3 objects. + // + // Parameter + // seg + // an object + // + // Return + // a boolean, true if valid + // + return Array.isArray(seg) && seg.length === 2 && + validatePoint(seg[0]) && validatePoint(seg[1]) +} diff --git a/lib/sphere2/boundingCircle.js b/lib/sphere2/boundingCircle.js new file mode 100644 index 00000000..97c428fa --- /dev/null +++ b/lib/sphere2/boundingCircle.js @@ -0,0 +1,60 @@ +module.exports = (circles) => { + // @affineplane.sphere2.boundingCircle(circles) + // + // Find a circle that encloses all the given circles. + // The result is approximate but is quaranteed to contain the optimal + // bounding circle. + // + // Parameters + // circles + // an array of circle2 + // + // Return + // a circle2 + // + + 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 + 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 + if (mix < minx) { minx = mix } + if (max > maxx) { maxx = max } + if (miy < miny) { miny = miy } + if (may > maxy) { maxy = may } + } + + // 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, + r: maxr + } +} diff --git a/lib/sphere2/index.js b/lib/sphere2/index.js index 1d6650f1..40cd4d60 100644 --- a/lib/sphere2/index.js +++ b/lib/sphere2/index.js @@ -1,13 +1,18 @@ // @affineplane.sphere2 +// @affineplane.circle2 // // Two dimensional sphere, a circle. // // Represented with an object `{ x, y, r }` for the origin and the radius. // +exports.UNIT = { x: 0, y: 0, r: 1 } +exports.ZERO = { x: 0, y: 0, r: 0 } + 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.copy = require('./copy') exports.create = require('./create') diff --git a/lib/sphere3/atCenter.js b/lib/sphere3/atCenter.js index b9b7c27d..c8713262 100644 --- a/lib/sphere3/atCenter.js +++ b/lib/sphere3/atCenter.js @@ -1,18 +1,13 @@ -module.exports = (sp) => { - // @affineplane.sphere3.atCenter(sp) - // - // Get the center point of the sphere. - // Note that the sphere3 object itself can act as a point3 in many cases. - // - // Parameters: - // a sphere3 - // - // Return - // a point3 - // - return { - x: sp.x, - y: sp.y, - z: sp.z - } -} +// @affineplane.sphere3.atCenter(sp) +// +// Get the center point of the sphere. +// Note that the sphere3 object itself can act as a point3 in many cases. +// +// Parameters: +// sp +// a sphere3 +// +// Return +// a point3 +// +module.exports = require('../point3/copy') diff --git a/lib/sphere3/boundingSphere.js b/lib/sphere3/boundingSphere.js new file mode 100644 index 00000000..6143dad8 --- /dev/null +++ b/lib/sphere3/boundingSphere.js @@ -0,0 +1,69 @@ +module.exports = (spheres) => { + // @affineplane.sphere3.boundingSphere(spheres) + // + // Find a sphere that encloses all the given spheres. + // The result is approximate but is quaranteed to contain the optimal + // bounding sphere. + // + // Parameters + // spheres + // an array of sphere3 + // + // Return + // a sphere3 + // + + const n = spheres.length + if (n === 0) { + throw new Error('Cannot compute bounding sphere for empty set of spheres.') + } + + // Find bounding box + const c0 = spheres[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 minz = c0.z - c0.r + let maxz = c0.z + c0.r + for (let i = 1; i < n; i += 1) { + const c = spheres[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 miz = c.z - c.r + const maz = c.z + c.r + if (mix < minx) { minx = mix } + if (max > maxx) { maxx = max } + if (miy < miny) { miny = miy } + if (may > maxy) { maxy = may } + if (miz < minz) { minz = miz } + 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 + const oz = minz + (maxz - minz) / 2 + + // Find max radius + let maxr = 0 + for (let i = 0; i < n; i += 1) { + const c = spheres[i] + const dx = c.x - ox + const dy = c.y - oy + const dz = c.z - oz + const d = Math.sqrt(dx * dx + dy * dy + dz * dz) + c.r + if (maxr < d) { maxr = d } + } + + return { + x: ox, + y: oy, + z: oz, + r: maxr + } +} diff --git a/lib/sphere3/index.js b/lib/sphere3/index.js index 83794355..20c8d67f 100644 --- a/lib/sphere3/index.js +++ b/lib/sphere3/index.js @@ -4,10 +4,14 @@ // // Represented with an object `{ x, y, z, r }` for the origin and the radius. // +exports.UNIT = { x: 0, y: 0, z: 0, r: 1 } +exports.ZERO = { x: 0, y: 0, z: 0, r: 0 } + exports.almostEqual = require('./almostEqual') exports.area = require('./area') exports.atCenter = require('./atCenter') exports.boundingBox = require('./boundingBox') +exports.boundingSphere = require('./boundingSphere') exports.collide = require('./collide') exports.copy = require('./copy') exports.create = require('./create') diff --git a/lib/vec2/index.js b/lib/vec2/index.js index 7c11c798..f9a8ee75 100644 --- a/lib/vec2/index.js +++ b/lib/vec2/index.js @@ -6,83 +6,50 @@ // // ![A vector](geometry_vector.png) // +exports.ZERO = { x: 0, y: 0 } -exports.add = require('./add') +// exports.distance +// Vectors do not have distance. See magnitude. +exports.add = require('./add') exports.almostEqual = require('./almostEqual') - exports.average = require('./average') - exports.copy = require('./copy') - exports.create = require('./create') - exports.cross = require('./cross') - exports.diff = require('./difference') exports.difference = exports.diff - exports.divide = require('./divide') - -// exports.distance -// Vectors do not have distance. See magnitude. - exports.dot = require('./dot') - exports.equal = require('./equal') - exports.fromArray = require('./fromArray') - exports.fromPolar = require('./fromPolar') exports.polar = exports.fromPolar - exports.independent = require('./independent') - exports.inverse = require('./invert') exports.invert = exports.inverse - exports.magnitude = require('./magnitude') - exports.max = require('./max') - exports.mean = exports.average - exports.min = require('./min') - exports.negate = exports.inverse - exports.norm = exports.magnitude - exports.normalize = require('./unit') - exports.opposite = exports.negation - -// exports.polarAverage - +// TODO MAYBE exports.polarAverage exports.projectToPlane = require('./projectToPlane') exports.projectTo = exports.projectToPlane exports.projectToVector = require('./projectToVector') - exports.rotateBy = require('./rotateBy') - exports.rotateTo = require('./rotateTo') - exports.scaleBy = require('./scaleBy') exports.scaleTo = require('./scaleTo') - exports.subtract = exports.diff - exports.sum = require('./sum') - exports.toArray = require('./toArray') - exports.toPolar = require('./toPolar') - exports.transformBy = require('./transformBy') - exports.transitFrom = require('./transitFrom') exports.transitTo = require('./transitTo') - exports.unit = exports.normalize - exports.validate = require('./validate') diff --git a/lib/vec3/index.js b/lib/vec3/index.js index 9af09b86..cfaada50 100644 --- a/lib/vec3/index.js +++ b/lib/vec3/index.js @@ -5,6 +5,8 @@ // and therefore their coordinates are affected only by plane scale // and rotation when represented on different plane. // +exports.ZERO = { x: 0, y: 0, z: 0 } + exports.add = require('./add') exports.almostEqual = require('./almostEqual') exports.average = require('./average') diff --git a/lib/vec4/index.js b/lib/vec4/index.js index ba308106..4107a69e 100644 --- a/lib/vec4/index.js +++ b/lib/vec4/index.js @@ -2,6 +2,7 @@ // // A vec4 is a 4D vector { x, y, z, w }. // +exports.ZERO = { x: 0, y: 0, z: 0, w: 0 } exports.add = require('./add') exports.create = require('./create') diff --git a/lib/version.js b/lib/version.js index 0d24b947..2589a27f 100644 --- a/lib/version.js +++ b/lib/version.js @@ -1,2 +1,2 @@ // Generated by genversion. -module.exports = '2.13.0' +module.exports = '2.14.0' diff --git a/package.json b/package.json index 01f95bdd..4271743c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "affineplane", - "version": "2.13.0", + "version": "2.14.0", "description": "Affine plane geometry library", "keywords": [ "affine", diff --git a/test/circle3/almostEqual.test.js b/test/circle3/almostEqual.test.js new file mode 100644 index 00000000..87c3e46a --- /dev/null +++ b/test/circle3/almostEqual.test.js @@ -0,0 +1,15 @@ +const fine = require('../../index') // should be exported +const circle3 = fine.circle3 + +module.exports = (ts) => { + ts.test('case: margin', (t) => { + const p = { x: 0, y: 0, z: 0, r: 0 } + const q = { x: 3, y: 3, z: 3, r: 3 } + t.false(circle3.almostEqual(p, q, 1)) + t.false(circle3.almostEqual(p, q, 11)) + t.true(circle3.almostEqual(p, q, 12)) + t.true(circle3.almostEqual(p, q, 13)) + + t.end() + }) +} diff --git a/test/circle3/boundaries.test.js b/test/circle3/boundaries.test.js new file mode 100644 index 00000000..96bbc32a --- /dev/null +++ b/test/circle3/boundaries.test.js @@ -0,0 +1,19 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic bounding box', (t) => { + t.deepEqual( + circle3.boundingBox({ x: 0, y: 0, z: 0, r: 0 }), + { a: 1, b: 0, x: 0, y: 0, z: 0, w: 0, h: 0, d: 0 }, + 'should be zero box' + ) + + t.deepEqual( + circle3.boundingBox({ x: 0, y: 0, z: 0, r: 1 }), + { a: 1, b: 0, x: -1, y: -1, z: 0, w: 2, h: 2, d: 0 }, + 'should be correct box' + ) + + t.end() + }) +} diff --git a/test/circle3/collisions.test.js b/test/circle3/collisions.test.js new file mode 100644 index 00000000..6432f23c --- /dev/null +++ b/test/circle3/collisions.test.js @@ -0,0 +1,95 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic point collision', (t) => { + t.true( + circle3.hasPoint({ x: 0, y: 0, z: 0, r: 0 }, { x: 0, y: 0, z: 0 }), + 'same origin collide' + ) + + t.false( + circle3.hasPoint({ x: 1, y: 0, z: 0, r: 0 }, { x: 0, y: 0, z: 0 }), + 'offset zero circle does not collide' + ) + + t.true( + circle3.hasPoint({ x: 1, y: 1, z: 1, r: 3 }, { x: 2, y: 2, z: 1 }), + 'point inside' + ) + + t.false( + circle3.hasPoint({ x: 0, y: 0, z: 0, r: 0 }, { x: 0, y: 0, z: 1 }), + 'point offset along z' + ) + + t.end() + }) + + ts.test('case: basic circle collision', (t) => { + let cc, c + + cc = { x: 0, y: 0, z: 0, r: 0 } + c = { x: 0, y: 0, z: 0, r: 1 } + t.true( + circle3.collide(cc, c), + 'same origin collide' + ) + + cc = { x: 0, y: 0, z: 0, r: 1 } + c = { x: 0, y: 0, z: 1, r: 1 } + t.false( + circle3.collideCircle(cc, c), + 'z offset do not collide' + ) + + cc = { x: -1, y: 0, z: 0, r: 1 } + c = { x: 1, y: 0, z: 0, r: 1 } + t.true( + circle3.collideCircle(cc, c), + 'edges touch' + ) + + t.end() + }) + + ts.test('case: circle-segment collision', (t) => { + let c, seg + + c = { x: 0, y: 0, z: 0, r: 0 } + seg = [{ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }] + t.true( + circle3.collideSegment(c, seg), + 'zero should collide if at the same point' + ) + + c = { x: 0, y: 0, z: 0, r: 0 } + seg = [{ x: 0, y: 0, z: -1 }, { x: 0, y: 0, z: 1 }] + t.true( + circle3.collideSegment(c, seg), + 'should collide if segment goes through pole' + ) + + c = { x: 1, y: 0, z: 0, r: 0 } + seg = [{ x: 0, y: 0, z: -1 }, { x: 0, y: 0, z: 1 }] + t.false( + circle3.collideSegment(c, seg), + 'should not collide if segment off' + ) + + c = { x: 0, y: 0, z: 0, r: 1 } + seg = [{ x: 0, y: 0, z: -1 }, { x: 0, y: 0, z: -2 }] + t.false( + circle3.collideSegment(c, seg), + 'should not collide if segment outside' + ) + + c = { x: 0, y: 0, z: 0, r: 1 } + seg = [{ x: 1, y: 0, z: -1 }, { x: 1, y: 0, z: 1 }] + t.true( + circle3.collideSegment(c, seg), + 'should collide if segment intersects circle edge' + ) + + t.end() + }) +} diff --git a/test/circle3/copy.test.js b/test/circle3/copy.test.js new file mode 100644 index 00000000..a3c6487d --- /dev/null +++ b/test/circle3/copy.test.js @@ -0,0 +1,19 @@ +const fine = require('../../index') // should be exported +const circle3 = fine.circle3 + +module.exports = (ts) => { + ts.test('case: copy, create', (t) => { + t.deepEqual( + circle3.create(1, 2, 3, 4), + { x: 1, y: 2, z: 3, r: 4 }, + 'should create valid' + ) + + const circ = { x: 1, y: 2, z: 3, r: 4 } + const copy = circle3.copy(circ) + t.deepEqual(copy, circ, 'should be similar') + t.notEqual(copy, circ, 'should not be same') + + t.end() + }) +} diff --git a/test/circle3/index.test.js b/test/circle3/index.test.js new file mode 100644 index 00000000..fa642113 --- /dev/null +++ b/test/circle3/index.test.js @@ -0,0 +1,19 @@ +// A unit for each method. +const units = { + almostEqual: require('./almostEqual.test'), + boundaries: require('./boundaries.test'), + collisions: require('./collisions.test'), + copy: require('./copy.test'), + measures: require('./measures.test'), + positions: require('./positions.test'), + projections: require('./projections.test'), + transformations: require('./transformations.test'), + transitions: require('./transitions.test'), + validate: require('./validate.test') +} + +module.exports = (t) => { + Object.keys(units).forEach((unitName) => { + t.test('affineplane.circle3.' + unitName, units[unitName]) + }) +} diff --git a/test/circle3/measures.test.js b/test/circle3/measures.test.js new file mode 100644 index 00000000..b3dd6c7e --- /dev/null +++ b/test/circle3/measures.test.js @@ -0,0 +1,29 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic area', (t) => { + t.equal( + circle3.area({ x: 0, y: 0, z: 0, r: 0 }), + 0, + 'should be zero area' + ) + + t.equal( + circle3.area({ x: 1, y: 1, z: 1, r: 1 }), + Math.PI, + 'should not be affected by translation' + ) + + t.end() + }) + + ts.test('case: basic circle size', (t) => { + t.deepEqual( + circle3.size({ x: 0, y: 0, z: 0, r: 1 }), + { w: 2, h: 2, d: 0 }, + 'should have zero depth' + ) + + t.end() + }) +} diff --git a/test/circle3/positions.test.js b/test/circle3/positions.test.js new file mode 100644 index 00000000..84458a82 --- /dev/null +++ b/test/circle3/positions.test.js @@ -0,0 +1,13 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic atCenter', (t) => { + t.deepEqual( + circle3.atCenter({ x: 1, y: 1, z: 1, r: 1 }), + { x: 1, y: 1, z: 1 }, + 'should drop radius' + ) + + t.end() + }) +} diff --git a/test/circle3/projections.test.js b/test/circle3/projections.test.js new file mode 100644 index 00000000..373df0d7 --- /dev/null +++ b/test/circle3/projections.test.js @@ -0,0 +1,38 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic orthogonal projection to plane', (t) => { + const plane = { a: 1, b: 0, x: 1, y: 2, z: 3 } + t.deepEqual( + circle3.projectTo({ x: 3, y: 0, z: 3, r: 3 }, plane), + { x: 2, y: -2, r: 3 }, + 'should translate' + ) + + t.end() + }) + + ts.test('case: basic perspective projection to plane', (t) => { + let circle, plane, camera + + circle = { x: 3, y: 0, z: 0, r: 1 } + plane = { a: 1, b: 0, x: 0, y: 0, z: 0 } + camera = { x: 1, y: 1, z: 0 } + t.deepEqual( + circle3.projectTo(circle, plane, camera), + { x: 1, y: 1, r: 0 }, + 'camera at geometry depth' + ) + + circle = { x: 2, y: 0, z: 0, r: 2 } + plane = { a: 1, b: 0, x: 0, y: 0, z: -2 } + camera = { x: 0, y: 0, z: -4 } + t.deepEqual( + circle3.projectTo(circle, plane, camera), + { x: 1, y: 0, r: 1 }, + 'camera away from projection plane' + ) + + t.end() + }) +} diff --git a/test/circle3/transformations.test.js b/test/circle3/transformations.test.js new file mode 100644 index 00000000..8c1b0cb8 --- /dev/null +++ b/test/circle3/transformations.test.js @@ -0,0 +1,128 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic offset', (t) => { + t.deepEqual( + circle3.offset({ x: 0, y: 0, z: 0, r: 1 }, 1, 1, 1), + { x: 1, y: 1, z: 1, r: 1 }, + 'should move by unit' + ) + + t.deepEqual( + circle3.offset({ x: 3, y: -3, z: 3, r: 1 }, 1, 1, 1), + { x: 4, y: -2, z: 4, r: 1 }, + 'non zero center point should move by unit' + ) + + t.end() + }) + + ts.test('case: basic translate', (t) => { + t.deepEqual( + circle3.translate({ x: 0, y: 0, z: 0, r: 1 }, { x: 1, y: 1, z: 1 }), + { x: 1, y: 1, z: 1, r: 1 }, + 'should move by unit' + ) + + t.deepEqual( + circle3.translate({ x: 3, y: -3, z: 3, r: 1 }, { x: 1, y: 1, z: 1 }), + { x: 4, y: -2, z: 4, r: 1 }, + 'non zero center point should move by unit' + ) + + t.end() + }) + + ts.test('case: basic polar offset', (t) => { + t.deepEqual( + circle3.polarOffset({ x: 0, y: 0, z: 0, r: 1 }, 1, 0), + { x: 1, y: 0, z: 0, r: 1 }, + 'should move towards x' + ) + + t.deepEqual( + circle3.polarOffset({ x: 3, y: -3, z: 3, r: 1 }, 1, 2, 0), + { x: 3, y: -3, z: 4, r: 1 }, + 'should move towards z, theta does not matter' + ) + + t.end() + }) + + ts.test('case: basic homothety', (t) => { + let circle, point + + circle = { x: 0, y: 0, z: 0, r: 0 } + point = { x: 0, y: 0, z: 0 } + t.deepEqual( + circle3.homothety(circle, point, 1), + { x: 0, y: 0, z: 0, r: 0 }, + 'origin at circle center' + ) + + circle = { x: 200, y: 200, z: 200, r: 100 } + point = { x: 0, y: 0, z: 0 } + t.deepEqual( + circle3.homothety(circle, point, 0), + { x: 0, y: 0, z: 0, r: 0 }, + 'zero factor towards zero' + ) + + circle = { x: 200, y: 200, z: 200, r: 100 } + point = { x: 1, y: 1, z: 1 } + t.deepEqual( + circle3.homothety(circle, point, 0), + { x: 1, y: 1, z: 1, r: 0 }, + 'zero factor towards non-zero' + ) + + circle = { x: 2, y: -2, z: 2, r: 2 } + point = { x: 0, y: 0, z: 0 } + t.deepEqual( + circle3.scaleBy(circle, point, 0.5), + { x: 1, y: -1, z: 1, r: 1 }, + 'half, alias' + ) + + circle = { x: 2, y: 2, z: 2, r: 2 } + point = { x: 2, y: 0, z: 0 } + t.deepEqual( + circle3.scaleBy(circle, point, 0.5), + { x: 2, y: 1, z: 1, r: 1 }, + 'half along y axis, alias' + ) + + t.end() + }) + + ts.test('case: basic rotate by z', (t) => { + let c, origin + const PI = Math.PI + + c = { x: 0, y: 0, z: 0, r: 0 } + origin = { x: 0, y: 0, z: 0 } + t.deepEqual( + circle3.rotateBy(c, origin, 0), + { x: 0, y: 0, z: 0, r: 0 }, + 'trivial zero' + ) + + c = { x: 0, y: 0, z: 2, r: 1 } + origin = { x: 0, y: 0, z: 1 } + t.almostEqualCircle( + circle3.rotateBy(c, origin, PI), + { x: 0, y: 0, z: 2, r: 1 }, + 'should rotate around z axis, same origin' + ) + + c = { x: 0, y: 0, z: 2, r: 1 } + origin = { x: 0, y: 2, z: 1 } + t.almostEqualCircle( + circle3.rotateBy(c, origin, PI), + { x: 0, y: 4, z: 2, r: 1 }, + 'should rotate around z axis' + ) + + t.end() + }) +} diff --git a/test/circle3/transitions.test.js b/test/circle3/transitions.test.js new file mode 100644 index 00000000..7de66a7f --- /dev/null +++ b/test/circle3/transitions.test.js @@ -0,0 +1,34 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: basic transit from plane', (t) => { + let circle, plane + + circle = { x: 2, y: 3, z: 4, r: 4 } + plane = { a: 1, b: 0, x: 2, y: 3, z: 4 } + t.deepEqual( + circle3.transitFrom(circle, plane), + { x: 4, y: 6, z: 8, r: 4 }, + 'translation' + ) + + circle = { x: 2, y: 3, z: 4, r: 4 } + plane = { a: 2, b: 0, x: 2, y: 3, z: 4 } + t.deepEqual( + circle3.transitFrom(circle, plane), + { x: 6, y: 9, z: 12, r: 8 }, + 'scaling and translation' + ) + + t.end() + }) + + ts.test('case: basic transit to plane', (t) => { + t.deepEqual(circle3.transitTo( + { x: 20, y: 30, z: 40, r: 40 }, + { a: 1, b: 0, x: 20, y: 30, z: 40 } + ), { x: 0, y: 0, z: 0, r: 40 }) + + t.end() + }) +} diff --git a/test/circle3/validate.test.js b/test/circle3/validate.test.js new file mode 100644 index 00000000..09b97634 --- /dev/null +++ b/test/circle3/validate.test.js @@ -0,0 +1,26 @@ +const circle3 = require('../../lib/circle3') + +module.exports = (ts) => { + ts.test('case: valid circle3', (t) => { + t.ok(circle3.validate({ x: 0, y: 1, z: 0, r: 1 })) + t.ok( + circle3.validate({ x: 0, y: 1, z: 0, r: 1, a: 2 }), + 'allow additional props' + ) + + t.end() + }) + + ts.test('case: invalid circle3', (t) => { + t.notOk(circle3.validate(null), 'detect null') + t.notOk(circle3.validate({}), 'detect empty object') + t.notOk(circle3.validate({ x: 0, y: 0, z: 0 }), 'detect missing prop') + t.notOk( + circle3.validate({ x: 0, y: 0, z: '1', r: 1 }), + 'detect string value' + ) + t.notOk(circle3.validate({ x: 0, y: 0, z: NaN, r: 1 }), 'detect NaN') + + t.end() + }) +} diff --git a/test/helm2/index.test.js b/test/helm2/index.test.js index d13a821c..746c9667 100644 --- a/test/helm2/index.test.js +++ b/test/helm2/index.test.js @@ -19,6 +19,7 @@ const units = { inverse: require('./inverse.test'), limitDilation: require('./limitDilation.test'), multiply: require('./multiply.test'), + projectToCameraTransform: require('./projectToCameraTransform.test'), projectToPlane: require('./projectToPlane.test'), rotateBy: require('./rotateBy.test'), scaleBy: require('./scaleBy.test'), diff --git a/test/helm2/projectToCameraTransform.test.js b/test/helm2/projectToCameraTransform.test.js new file mode 100644 index 00000000..8c85d9e0 --- /dev/null +++ b/test/helm2/projectToCameraTransform.test.js @@ -0,0 +1,43 @@ +const helm2 = require('../../lib/helm2') + +module.exports = (ts) => { + ts.test('case: project to camera transform', (t) => { + let tr, origin, camera + + tr = { a: 1, b: 0, x: 0, y: 0 } + origin = { x: 0, y: 0, z: 0 } + camera = { x: 0, y: 0, z: -1 } + t.almostEqualHelmert( + helm2.projectToCameraTransform(tr, origin, camera), + { a: 1, b: 0, x: 0, y: 0, z: 0 }, + 'trivial noop projection' + ) + + tr = { a: 2, b: 0, x: 0, y: 0 } + origin = { x: 0, y: 0, z: 0 } + camera = { x: 0, y: 0, z: -1 } + t.almostEqualHelmert( + helm2.projectToCameraTransform(tr, origin, camera), + { a: 1, b: 0, x: 0, y: 0, z: 0.5 }, + 'transform on the plane, travel a bit closer' + ) + + tr = { a: 0, b: 2, x: 2, y: 2 } + origin = { x: 0, y: 0, z: 2 } + camera = { x: 0, y: 0, z: 0 } + t.almostEqualHelmert( + helm2.projectToCameraTransform(tr, origin, camera), + { a: 0, b: -1, x: -1, y: -1, z: 1 }, + 'should invert rotation and invert and scale translation' + ) + + tr = { a: 0, b: 0, x: 2, y: 2 } + t.almostEqualHelmert( + helm2.projectToCameraTransform(tr, origin, camera), + { a: 1, b: 0, x: 0, y: 0, z: 0 }, + 'should handle singular helmert' + ) + + t.end() + }) +} diff --git a/test/helm3/difference.test.js b/test/helm3/difference.test.js new file mode 100644 index 00000000..14b352d6 --- /dev/null +++ b/test/helm3/difference.test.js @@ -0,0 +1,25 @@ +const helm3 = require('../../lib/helm3') + +module.exports = (ts) => { + ts.test('case: basic difference', (t) => { + let h, hh + + h = { a: 1, b: 0, x: 1, y: 1, z: 1 } + hh = { a: 1, b: 0, x: 2, y: 2, z: 2 } + t.almostEqual( + helm3.difference(h, hh), + { a: 1, b: 0, x: -1, y: -1, z: -1 }, + 'should work like vector difference' + ) + + h = { a: 1, b: 0, x: 1, y: 1, z: 1 } + hh = { a: 2, b: 0, x: 2, y: 2, z: 2 } + t.almostEqual( + helm3.difference(h, hh), + { a: 0.5, b: 0, x: 0, y: 0, z: 0 }, + 'should return transformation' + ) + + t.end() + }) +} diff --git a/test/helm3/index.test.js b/test/helm3/index.test.js index d44b0300..4e0a2f54 100644 --- a/test/helm3/index.test.js +++ b/test/helm3/index.test.js @@ -8,6 +8,7 @@ const units = { copy: require('./copy.test'), create: require('./create.test'), det: require('./det.test'), + difference: require('./difference.test'), equal: require('./equal.test'), fromArray: require('./fromArray.test'), fromBasisVector: require('./fromBasisVector.test'), diff --git a/test/index.test.js b/test/index.test.js index 886aea07..0508a724 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -6,11 +6,12 @@ const units = { 'affineplane.angle': require('./angle/index.test'), 'affineplane.box2': require('./box2/index.test'), 'affineplane.box3': require('./box3/index.test'), - 'affineplane.epsilon': require('./epsilon/index.test'), + 'affineplane.circle3': require('./circle3/index.test'), 'affineplane.dir2': require('./dir2/index.test'), 'affineplane.dir3': require('./dir3/index.test'), 'affineplane.dist2': require('./dist2/index.test'), 'affineplane.dist3': require('./dist3/index.test'), + 'affineplane.epsilon': require('./epsilon/index.test'), 'affineplane.helm2': require('./helm2/index.test'), 'affineplane.helm3': require('./helm3/index.test'), 'affineplane.line2': require('./line2/index.test'), @@ -30,6 +31,7 @@ const units = { 'affineplane.scalar2': require('./scalar2/index.test'), 'affineplane.scalar3': require('./scalar3/index.test'), 'affineplane.segment2': require('./segment2/index.test'), + 'affineplane.segment3': require('./segment3/index.test'), 'affineplane.sphere2': require('./sphere2/index.test'), 'affineplane.sphere3': require('./sphere3/index.test'), 'affineplane.size2': require('./size2/index.test'), @@ -47,8 +49,10 @@ test.Test.prototype.notAlmostEqual = require('./utils/notAlmostEqual') test.Test.prototype.transformEqual = require('./utils/transformEqual') test.Test.prototype.almostEqualBasis = require('./utils/almostEqualBasis') test.Test.prototype.almostEqualBox = require('./utils/almostEqualBox') +test.Test.prototype.almostEqualCircle = require('./utils/almostEqualSphere') +test.Test.prototype.almostEqualHelmert = require('./utils/almostEqualHelmert') test.Test.prototype.almostEqualPoint = require('./utils/almostEqualPoint') -test.Test.prototype.almostEqualSphere = require('./utils/almostEqualSphere') +test.Test.prototype.almostEqualSphere = test.Test.prototype.almostEqualCircle test.Test.prototype.almostEqualVector = require('./utils/almostEqualVector') // Run test suite diff --git a/test/plane2/difference.test.js b/test/plane2/difference.test.js index 55afd3c4..e0280898 100644 --- a/test/plane2/difference.test.js +++ b/test/plane2/difference.test.js @@ -7,8 +7,8 @@ module.exports = (ts) => { t.almostEqual( plane2.between(source, target), - { a: 0.5, b: 0, x: -0.5, y: -0.5 }, - 'source represented on target' + { a: 0.5, b: 0, x: 0, y: 0 }, + 'transformation that would map target to source' ) t.end() diff --git a/test/plane2/index.test.js b/test/plane2/index.test.js index fefebbdd..2caf3464 100644 --- a/test/plane2/index.test.js +++ b/test/plane2/index.test.js @@ -11,6 +11,7 @@ const units = { getScale: require('./getScale.test'), invert: require('./invert.test'), limitScale: require('./limitScale.test'), + orientation: require('./orientation.test'), projectToPlane: require('./projectToPlane.test'), rotateBy: require('./rotateBy.test'), rotateTo: require('./rotateTo.test'), diff --git a/test/plane2/orientation.test.js b/test/plane2/orientation.test.js new file mode 100644 index 00000000..1fb9dc2d --- /dev/null +++ b/test/plane2/orientation.test.js @@ -0,0 +1,25 @@ +const plane2 = require('../../lib/plane2') + +module.exports = (ts) => { + ts.test('case: plane orientation', (t) => { + t.deepEqual( + plane2.orientation({ a: 1, b: 0, x: 0, y: 0 }), + { a: 1, b: 0 }, + 'trivial' + ) + + t.deepEqual( + plane2.orientation({ a: 0, b: 2, x: 0, y: 0 }), + { a: 0, b: 1 }, + 'should make unitary' + ) + + t.deepEqual( + plane2.orientation({ a: 0, b: 0, x: 0, y: 0 }), + { a: 1, b: 0 }, + 'should fall back to default' + ) + + t.end() + }) +} diff --git a/test/plane3/difference.test.js b/test/plane3/difference.test.js index 140779ee..642b2a35 100644 --- a/test/plane3/difference.test.js +++ b/test/plane3/difference.test.js @@ -7,8 +7,8 @@ module.exports = (ts) => { t.almostEqual( plane3.between(source, target), - { a: 0.5, b: 0, x: -0.5, y: -0.5, z: -0.5 }, - 'source represented on target' + { a: 0.5, b: 0, x: 0, y: 0, z: 0 }, + 'transformation that maps target to source' ) t.end() diff --git a/test/plane3/index.test.js b/test/plane3/index.test.js index 3560b231..33d5b037 100644 --- a/test/plane3/index.test.js +++ b/test/plane3/index.test.js @@ -12,8 +12,10 @@ const units = { getScale: require('./getScale.test'), invert: require('./invert.test'), limitScale: require('./limitScale.test'), - projectToPlane: require('./projectToPlane.test'), + orientation: require('./orientation.test'), + projectByDepth: require('./projectByDepth.test'), projectToDepth: require('./projectToDepth.test'), + projectToPlane: require('./projectToPlane.test'), projectToScale: require('./projectToScale.test'), rotateBy: require('./rotateBy.test'), rotateTo: require('./rotateTo.test'), diff --git a/test/plane3/orientation.test.js b/test/plane3/orientation.test.js new file mode 100644 index 00000000..67146152 --- /dev/null +++ b/test/plane3/orientation.test.js @@ -0,0 +1,19 @@ +const plane3 = require('../../lib/plane3') + +module.exports = (ts) => { + ts.test('case: plane orientation', (t) => { + t.deepEqual( + plane3.orientation({ a: 1, b: 0, x: 0, y: 0, z: 0 }), + { a: 1, b: 0 }, + 'trivial' + ) + + t.deepEqual( + plane3.orientation({ a: 0, b: 2, x: 0, y: 0, z: 0 }), + { a: 0, b: 1 }, + 'should make unitary' + ) + + t.end() + }) +} diff --git a/test/plane3/projectByDepth.test.js b/test/plane3/projectByDepth.test.js new file mode 100644 index 00000000..d49ead84 --- /dev/null +++ b/test/plane3/projectByDepth.test.js @@ -0,0 +1,27 @@ +const plane3 = require('../../lib/plane3') + +module.exports = (ts) => { + ts.test('case: basic projectByDepth', (t) => { + let plane, origin, delta + + plane = { a: 1, b: 0, x: 0, y: 0, z: 0 } + origin = { x: 0, y: 0, z: 0 } + delta = 0 + t.deepEqual( + plane3.projectByDepth(plane, origin, delta), + { a: 1, b: 0, x: 0, y: 0, z: 0 }, + 'should return unmodified plane because origin on the plane' + ) + + plane = { a: 1, b: 0, x: 0, y: 0, z: 0 } + origin = { x: 0, y: 0, z: -1 } + delta = 1 + t.deepEqual( + plane3.projectByDepth(plane, origin, delta), + { a: 2, b: 0, x: 0, y: 0, z: 1 }, + 'should dilate and translate' + ) + + t.end() + }) +} diff --git a/test/point2/index.test.js b/test/point2/index.test.js index d778cfdc..b46f12e3 100644 --- a/test/point2/index.test.js +++ b/test/point2/index.test.js @@ -6,6 +6,7 @@ const units = { direction: require('./direction.test'), homothety: require('./homothety.test'), offset: require('./offset.test'), + projectByDistance: require('./projectByDistance.test'), projectToLine: require('./projectToLine.test'), projectToPlane: require('./projectToPlane.test'), transformMany: require('./transformMany.test'), diff --git a/test/point2/projectByDistance.test.js b/test/point2/projectByDistance.test.js new file mode 100644 index 00000000..76cf44eb --- /dev/null +++ b/test/point2/projectByDistance.test.js @@ -0,0 +1,34 @@ +const point2 = require('../../lib/point2') + +module.exports = (ts) => { + ts.test('case: project by distance', (t) => { + const origin = { x: 0, y: 0 } + const point = { x: 1, y: 0 } + + t.deepEqual( + point2.projectByDistance(point, origin, 1), + { x: 2, y: 0 }, + 'project one unit ahead' + ) + + t.deepEqual( + point2.projectByDistance(point, origin, -1), + { x: 0, y: 0 }, + 'project one unit back' + ) + + t.deepEqual( + point2.projectByDistance(point, origin, 0), + { x: 1, y: 0 }, + 'project zero units' + ) + + t.deepEqual( + point2.projectByDistance(point, point, 1), + { x: 1, y: 0 }, + 'should not project if impossible' + ) + + t.end() + }) +} diff --git a/test/point3/distanceToPlane.test.js b/test/point3/distanceToPlane.test.js new file mode 100644 index 00000000..6fbf3185 --- /dev/null +++ b/test/point3/distanceToPlane.test.js @@ -0,0 +1,25 @@ +const point3 = require('../../lib/point3') + +module.exports = (ts) => { + ts.test('case: distance to plane', (t) => { + let point, plane + + point = { x: 0, y: 0, z: 0 } + plane = { a: 1, b: 0, x: 0, y: 0, z: 0 } + t.equal( + point3.distanceToPlane(point, plane), + 0, + 'trivial' + ) + + point = { x: 20, y: 20, z: 1 } + plane = { a: 1, b: 0, x: 0, y: 0, z: 0 } + t.equal( + point3.distanceToPlane(point, plane), + 1, + 'only depth matters' + ) + + t.end() + }) +} diff --git a/test/point3/index.test.js b/test/point3/index.test.js index 3d2b081e..b3ddf9de 100644 --- a/test/point3/index.test.js +++ b/test/point3/index.test.js @@ -6,11 +6,13 @@ const units = { difference: require('./difference.test'), direction: require('./direction.test'), distance: require('./distance.test'), + distanceToPlane: require('./distanceToPlane.test'), equal: require('./equal.test'), fromArray: require('./fromArray.test'), homothety: require('./homothety.test'), offset: require('./offset.test'), polarOffset: require('./polarOffset.test'), + projectByDistance: require('./projectByDistance.test'), projectToPlane: require('./projectToPlane.test'), rotateAroundLine: require('./rotateAroundLine.test'), rotateBy: require('./rotateBy.test'), diff --git a/test/point3/projectByDistance.test.js b/test/point3/projectByDistance.test.js new file mode 100644 index 00000000..aef82cea --- /dev/null +++ b/test/point3/projectByDistance.test.js @@ -0,0 +1,35 @@ +const point3 = require('../../lib/point3') + +module.exports = (ts) => { + ts.test('case: project by distance', (t) => { + const origin = { x: 0, y: 0, z: 0 } + const point = { x: 1, y: 0, z: 1 } + const dist = Math.sqrt(2) + + t.deepEqual( + point3.projectByDistance(point, origin, dist), + { x: 2, y: 0, z: 2 }, + 'project one unit ahead' + ) + + t.deepEqual( + point3.projectByDistance(point, origin, -dist), + { x: 0, y: 0, z: 0 }, + 'project one unit back' + ) + + t.deepEqual( + point3.projectByDistance(point, origin, 0), + { x: 1, y: 0, z: 1 }, + 'project zero units' + ) + + t.deepEqual( + point3.projectByDistance(point, point, 1), + { x: 1, y: 0, z: 1 }, + 'should not project if impossible' + ) + + t.end() + }) +} diff --git a/test/scalar1/index.test.js b/test/scalar1/index.test.js index 10b35bc6..f01c4fe0 100644 --- a/test/scalar1/index.test.js +++ b/test/scalar1/index.test.js @@ -3,6 +3,7 @@ const units = { almostEqual: require('./almostEqual.test'), create: require('./create.test'), equal: require('./equal.test'), + projectToPlane: require('./projectToPlane.test'), transitFrom: require('./transitFrom.test'), transitTo: require('./transitTo.test'), validate: require('./validate.test') diff --git a/test/scalar1/projectToPlane.test.js b/test/scalar1/projectToPlane.test.js new file mode 100644 index 00000000..b29730fd --- /dev/null +++ b/test/scalar1/projectToPlane.test.js @@ -0,0 +1,51 @@ +const scalar1 = require('../../lib/scalar1') + +module.exports = (ts) => { + ts.test('case: basic orthogonal projection to plane', (t) => { + const s = 2 + const planea = { a: 1, b: 0, x: 0, y: 0, z: 0 } + t.deepEqual( + scalar1.projectToPlane(s, planea), + s, + 'identity' + ) + + const planeb = { a: 0, b: 2, x: 100, y: 100, z: 100 } + t.deepEqual( + scalar1.projectToPlane(s, planeb), + 1, + 'only scale should matter' + ) + + t.end() + }) + + ts.test('case: basic perspective projection to plane', (t) => { + const s = 2 + const planea = { a: 1, b: 0, x: 0, y: 0, z: 0 } + const cameraa = { x: 1, y: 1, z: 0 } + t.deepEqual( + scalar1.projectToPlane(s, planea, cameraa), + 0, + 'camera at geometry depth' + ) + + const planeb = { a: 1, b: 0, x: 0, y: 0, z: -2 } + const camerab = { x: 0, y: 0, z: -4 } + t.deepEqual( + scalar1.projectToPlane(s, planeb, camerab), + 1, // halved due perspective + 'camera away from projection plane' + ) + + const planec = { a: 3, b: 0, x: 0, y: 0, z: -2 } + const camerac = { x: 0, y: 0, z: -4 } + t.deepEqual( + scalar1.projectToPlane(s, planec, camerac), + 1 / 3, // halved due perspective, thirded due plane scale + 'scaled projection plane' + ) + + t.end() + }) +} diff --git a/test/scalar2/index.test.js b/test/scalar2/index.test.js index 67cdc69b..cd1682c4 100644 --- a/test/scalar2/index.test.js +++ b/test/scalar2/index.test.js @@ -3,6 +3,7 @@ const units = { almostEqual: require('./almostEqual.test'), create: require('./create.test'), equal: require('./equal.test'), + projectToPlane: require('./projectToPlane.test'), transitFrom: require('./transitFrom.test'), transitTo: require('./transitTo.test'), validate: require('./validate.test') diff --git a/test/scalar2/projectToPlane.test.js b/test/scalar2/projectToPlane.test.js new file mode 100644 index 00000000..7c518ae4 --- /dev/null +++ b/test/scalar2/projectToPlane.test.js @@ -0,0 +1,51 @@ +const scalar2 = require('../../lib/scalar2') + +module.exports = (ts) => { + ts.test('case: basic orthogonal projection to plane', (t) => { + const ss = 2 + const planea = { a: 1, b: 0, x: 0, y: 0, z: 0 } + t.deepEqual( + scalar2.projectToPlane(ss, planea), + ss, + 'identity' + ) + + const planeb = { a: 0, b: 2, x: 100, y: 100, z: 100 } + t.deepEqual( + scalar2.projectToPlane(ss, planeb), + 0.5, + 'only scale should matter' + ) + + t.end() + }) + + ts.test('case: basic perspective projection to plane', (t) => { + const ss = 2 + const planea = { a: 1, b: 0, x: 0, y: 0, z: 0 } + const cameraa = { x: 1, y: 1, z: 0 } + t.deepEqual( + scalar2.projectToPlane(ss, planea, cameraa), + 0, + 'camera at geometry depth' + ) + + const planeb = { a: 1, b: 0, x: 0, y: 0, z: -2 } + const camerab = { x: 0, y: 0, z: -4 } + t.deepEqual( + scalar2.projectToPlane(ss, planeb, camerab), + 0.5, // quartered due perspective + 'camera away from projection plane' + ) + + const planec = { a: 3, b: 0, x: 0, y: 0, z: -2 } + const camerac = { x: 0, y: 0, z: -4 } + t.almostEqual( + scalar2.projectToPlane(ss, planec, camerac), + 1 / 18, // 1/4 due perspective, 1/9 due plane scale. 2 Initially. + 'scaled projection plane' + ) + + t.end() + }) +} diff --git a/test/segment3/conversions.test.js b/test/segment3/conversions.test.js new file mode 100644 index 00000000..7fbc246d --- /dev/null +++ b/test/segment3/conversions.test.js @@ -0,0 +1,14 @@ +const affineplane = require('../../index') +const segment3 = affineplane.segment3 + +module.exports = (ts) => { + ts.test('case: toVector', (t) => { + t.deepEqual( + segment3.toVector([{ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 }]), + { x: 1, y: 1, z: 1 }, + 'should create a vector' + ) + + t.end() + }) +} diff --git a/test/segment3/create.test.js b/test/segment3/create.test.js new file mode 100644 index 00000000..aac1ce19 --- /dev/null +++ b/test/segment3/create.test.js @@ -0,0 +1,14 @@ +const affineplane = require('../../index') +const segment3 = affineplane.segment3 + +module.exports = (ts) => { + ts.test('case: basic create', (t) => { + t.deepEqual( + segment3.create({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 }), + [{ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 }], + 'should form an array' + ) + + t.end() + }) +} diff --git a/test/segment3/index.test.js b/test/segment3/index.test.js new file mode 100644 index 00000000..69940c91 --- /dev/null +++ b/test/segment3/index.test.js @@ -0,0 +1,13 @@ +// A unit for each method. +const units = { + conversions: require('./conversions.test'), + create: require('./create.test'), + transitions: require('./transitions.test'), + validate: require('./validate.test') +} + +module.exports = (t) => { + Object.keys(units).forEach((unitName) => { + t.test('affineplane.segment3.' + unitName, units[unitName]) + }) +} diff --git a/test/segment3/transitions.test.js b/test/segment3/transitions.test.js new file mode 100644 index 00000000..2cba6c01 --- /dev/null +++ b/test/segment3/transitions.test.js @@ -0,0 +1,34 @@ +const affineplane = require('../../index') +const segment3 = affineplane.segment3 + +module.exports = (ts) => { + ts.test('case: basic transitFrom', (t) => { + // let seg, plane + + const seg = [{ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 }] + const basis = { a: 2, b: 0, x: 0, y: 2, z: 1 } + + t.deepEqual( + segment3.transitFrom(seg, basis), + [{ x: 0, y: 2, z: 1 }, { x: 2, y: 4, z: 3 }], + 'scale and translation should affect' + ) + + t.end() + }) + + ts.test('case: basic transitTo', (t) => { + // let seg, plane + + const seg = [{ x: 0, y: 2, z: 1 }, { x: 2, y: 4, z: 3 }] + const plane = { a: 2, b: 0, x: 0, y: 2, z: 1 } + + t.deepEqual( + segment3.transitTo(seg, plane), + [{ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 }], + 'scale and translation should affect' + ) + + t.end() + }) +} diff --git a/test/segment3/validate.test.js b/test/segment3/validate.test.js new file mode 100644 index 00000000..b7372490 --- /dev/null +++ b/test/segment3/validate.test.js @@ -0,0 +1,25 @@ +const segment2 = require('../../index').segment2 + +module.exports = (ts) => { + ts.test('case: valid segment2', (t) => { + t.ok(segment2.validate([{ x: 1, y: 2 }, { x: 2, y: 3 }]), 'two points') + + t.end() + }) + + ts.test('case: invalid segment2', (t) => { + t.notOk(segment2.validate([]), 'detect empty') + t.notOk(segment2.validate(null), 'detect null') + t.notOk(segment2.validate({}), 'detect empty object') + t.notOk( + segment2.validate([{ x: 0, y: 1 }, { x: 0 }]), + 'detect invalid point' + ) + t.notOk( + segment2.validate([{ x: 0, y: 1 }, '0,1']), + 'detect string' + ) + + t.end() + }) +} diff --git a/test/sphere2/boundingCircle.test.js b/test/sphere2/boundingCircle.test.js new file mode 100644 index 00000000..675756e6 --- /dev/null +++ b/test/sphere2/boundingCircle.test.js @@ -0,0 +1,24 @@ +const sphere2 = require('../../index').sphere2 + +module.exports = (ts) => { + ts.test('case: bounding circle of circles', (t) => { + t.deepEqual( + sphere2.boundingCircle([ + { x: 0, y: 0, r: 1 } + ]), + { x: 0, y: 0, r: 1 }, + 'trivial single' + ) + + t.deepEqual( + sphere2.boundingCircle([ + { x: 0, y: 0, r: 1 }, + { x: 0, y: 2, r: 1 } + ]), + { x: 0, y: 1, r: 2 }, + 'should match exactly' + ) + + t.end() + }) +} diff --git a/test/sphere2/index.test.js b/test/sphere2/index.test.js index 70a09961..5e681b23 100644 --- a/test/sphere2/index.test.js +++ b/test/sphere2/index.test.js @@ -3,6 +3,7 @@ const units = { almostEqual: require('./almostEqual.test'), atCenter: require('./atCenter.test'), boundingBox: require('./boundingBox.test'), + boundingCircle: require('./boundingCircle.test'), collide: require('./collide.test'), gap: require('./gap.test'), hasPoint: require('./hasPoint.test'), diff --git a/test/sphere3/boundingSphere.test.js b/test/sphere3/boundingSphere.test.js new file mode 100644 index 00000000..654869c4 --- /dev/null +++ b/test/sphere3/boundingSphere.test.js @@ -0,0 +1,33 @@ +const sphere3 = require('../../index').sphere3 + +module.exports = (ts) => { + ts.test('case: bounding sphere of spheres', (t) => { + t.deepEqual( + sphere3.boundingSphere([ + { x: 0, y: 0, z: 0, r: 1 } + ]), + { x: 0, y: 0, z: 0, r: 1 }, + 'trivial single' + ) + + t.deepEqual( + sphere3.boundingSphere([ + { x: 0, y: 0, z: 0, r: 1 }, + { x: 0, y: 2, z: 0, r: 1 } + ]), + { x: 0, y: 1, z: 0, r: 2 }, + 'should fit exactly along y' + ) + + t.deepEqual( + sphere3.boundingSphere([ + { x: 0, y: 0, z: 0, r: 1 }, + { x: 0, y: 0, z: 2, r: 1 } + ]), + { x: 0, y: 0, z: 1, r: 2 }, + 'should fit exactly along z' + ) + + t.end() + }) +} diff --git a/test/sphere3/index.test.js b/test/sphere3/index.test.js index e70880e5..95723a37 100644 --- a/test/sphere3/index.test.js +++ b/test/sphere3/index.test.js @@ -3,6 +3,7 @@ const units = { almostEqual: require('./almostEqual.test'), atCenter: require('./atCenter.test'), boundingBox: require('./boundingBox.test'), + boundingSphere: require('./boundingSphere.test'), collide: require('./collide.test'), gap: require('./gap.test'), hasPoint: require('./hasPoint.test'), diff --git a/test/utils/almostEqualHelmert.js b/test/utils/almostEqualHelmert.js new file mode 100644 index 00000000..9207a0f1 --- /dev/null +++ b/test/utils/almostEqualHelmert.js @@ -0,0 +1,24 @@ +const helm2 = require('../../lib/helm2') +const helm3 = require('../../lib/helm3') + +module.exports = function (actual, expected, message) { + // Custom tape.js assertion. + + let isEqual = false + if (helm3.validate(expected)) { + // Expect helm3 + isEqual = helm3.validate(actual) + isEqual = isEqual && helm3.almostEqual(actual, expected) + } else if (helm2.validate(expected)) { + // Expect helm2 + isEqual = helm2.validate(actual) + isEqual = isEqual && helm2.almostEqual(actual, expected) + } + + this._assert(isEqual, { + message: message || 'helmert should have correct elements', + operator: 'almostEqualHelmert', + actual, + expected + }) +}