From 99222fa5c1986f37f7582bd07cadf79b7fe8f419 Mon Sep 17 00:00:00 2001 From: Akseli Palen Date: Sun, 7 Jul 2024 17:59:53 +0300 Subject: [PATCH 1/5] implement sphere2.tangentCircle --- lib/sphere2/index.js | 1 + lib/sphere2/tangentCircle.js | 135 +++++++++++++++++ test/sphere2/index.test.js | 1 + test/sphere2/tangentCircle.test.js | 223 +++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 lib/sphere2/tangentCircle.js create mode 100644 test/sphere2/tangentCircle.test.js diff --git a/lib/sphere2/index.js b/lib/sphere2/index.js index b2ca71a..33c27da 100644 --- a/lib/sphere2/index.js +++ b/lib/sphere2/index.js @@ -36,6 +36,7 @@ exports.polarOffset = require('./polarOffset') exports.rotateBy = require('./rotateBy') exports.scaleBy = exports.homothety exports.size = require('./size') +exports.tangentCircle = require('./tangentCircle') exports.transitFrom = require('./transitFrom') exports.transitTo = require('./transitTo') exports.translate = require('./translate') diff --git a/lib/sphere2/tangentCircle.js b/lib/sphere2/tangentCircle.js new file mode 100644 index 0000000..a99ea14 --- /dev/null +++ b/lib/sphere2/tangentCircle.js @@ -0,0 +1,135 @@ +module.exports = (ca, cb, r, righthand) => { + // @affineplane.sphere2.tangentCircle(ca, cb, r, righthand) + // + // Find a circle C that is left-hand tangent to the circles A and B. + // Use the fourth parameter to switch to right-hand tangent. + // Note the coordinate system: x-axis points right and y-axis points down. + // + // If the gap between A and B is too large for C to be tangent to both + // then the resulting circle is tangent with A and as close to B as possible. + // Under the righthand flag, the preference is reversed. + // + // Parameters: + // ca + // a sphere2 {x,y,r}, the circle A. + // cb + // a sphere2 {x,y,r}, the circle B. + // r + // a number, a dist2, the radius of the circle C. + // righthand + // a boolean, default false. Set true to find right-hand tangent circle. + // + // Returns: + // a sphere2 {x,y,r} + // + // See also sphere2.tangentCircles for efficient computation of both hands with just one function call. + // + + // Sort circles largest first. This avoids special treatment of cases + // where the point H (the point C projected on the AB line) is outside AB. + // However, we need to maintain handedness with respect to the original argument order. + let swapped = false + let hand = (righthand ? 1 : -1) + if (ca.r < cb.r) { + // Swap + const ct = cb + cb = ca + ca = ct + swapped = true + hand = -hand + } + + // Vector from A to B, of length c. + const vab = { + x: cb.x - ca.x, + y: cb.y - ca.y + } + // Triangle sides + const a = cb.r + r // length of BC + const b = ca.r + r // length of AC + const c = Math.sqrt(vab.x * vab.x + vab.y * vab.y) // ca.r + cb.r for tangent A and B + // Check special cases. + const epsilon = 1000 * Number.EPSILON + if (-epsilon < c && c < epsilon) { + // The circles A and B are concentric i.e. the distance between their centers is zero. + // The circle C can be tangent to both only if A and B have equal radius. + // The best compromise is to let C be tangent to the one with the largest radius (A). + // The tangent position for the circle C is found at an arbitrary angle (choose 0 deg). + return { + x: ca.x + b, // because b = ca.r + r and ca.r is always >= cb.r. + y: ca.y, + r + } + } + if (c + cb.r <= ca.r) { + // The circle A fully covers the circle B. + // In other words, the circle B is completely inside the circle A. + // The best compromise is to let the circle C be tangent to the circle A + // at a position closest to the circle B. + const vac = { + x: vab.x * b / c, + y: vab.y * b / c + } + // The center point of the circle C: vc = va + vac + return { + x: ca.x + vac.x, + y: ca.y + vac.y, + r + } + } + if (a + b < c) { + // The circles A and B are so far away that the circle C cannot connect them. + // Find a circle that is tangent with A and has center point along AB. + // To maintain the original circle order regardless of their radii, + // we treat both orders separately. + if (!swapped !== !righthand) { // Coerce to bool, then XOR + // Swapped xor right-handed order. + // Prefer the circle B. + const vbc = { + x: -vab.x * a / c, + y: -vab.y * a / c + } + // The center point of the circle C: vc = vb + vbc + return { + x: cb.x + vbc.x, + y: cb.y + vbc.y, + r + } + } // else + // Original order. Prefer the circle A. + const vac = { + x: vab.x * b / c, + y: vab.y * b / c + } + // The center point of the circle C: vc = va + vac + return { + x: ca.x + vac.x, + y: ca.y + vac.y, + r + } + } + // After excluding the special cases above, + // it is now possible to find a circle that is + // tangent to both circles A and B. + const p = (a + b + c) / 2 + const A = Math.sqrt(p * (p - a) * (p - b) * (p - c)) + const h = 2 * A / c + const w = Math.sqrt(b * b - h * h) + // Vector from A to H, of length w. + const vw = { + x: vab.x * w / c, + y: vab.y * w / c + } + // Vector from H to C, of length h. + const vh = { + x: hand * -vw.y * h / w, + y: hand * vw.x * h / w + } + // Find the center point of C. + // A + vw + vh + return { + x: ca.x + vw.x + vh.x, + y: ca.y + vw.y + vh.y, + r + } +} diff --git a/test/sphere2/index.test.js b/test/sphere2/index.test.js index 6d3256c..3615a10 100644 --- a/test/sphere2/index.test.js +++ b/test/sphere2/index.test.js @@ -14,6 +14,7 @@ const units = { polarOffset: require('./polarOffset.test'), rotateBy: require('./rotateBy.test'), size: require('./size.test'), + tangentCircle: require('./tangentCircle.test'), transitFrom: require('./transitFrom.test'), transitTo: require('./transitTo.test'), validate: require('./validate.test') diff --git a/test/sphere2/tangentCircle.test.js b/test/sphere2/tangentCircle.test.js new file mode 100644 index 0000000..0b48eba --- /dev/null +++ b/test/sphere2/tangentCircle.test.js @@ -0,0 +1,223 @@ +const sphere2 = require('../../lib/sphere2') + +module.exports = (ts) => { + ts.test('case: basic tangent circle', (t) => { + t.deepEqual( + sphere2.tangentCircle( + { x: 0, y: 0, r: 1 }, + { x: 4, y: 0, r: 1 }, + 1 + ), + { x: 2, y: 0, r: 1 }, + 'should find a circle between' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 3, r: 2 }, + { x: 4, y: 0, r: 3 }, + 1 + ), + { x: 0, y: 0, r: 1 }, + 'should find at the left-hand side, in spite of smaller first' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 4, r: 3 }, + { x: 3, y: 0, r: 2 }, + 1 + ), + { x: 0, y: 0, r: 1 }, + 'should find at the left-hand side' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 3, y: 0, r: 2 }, + { x: 0, y: 4, r: 3 }, + 1, + true + ), + { x: 0, y: 0, r: 1 }, + 'should find at the right-hand side, in spite of smaller first' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 4, y: 0, r: 3 }, + { x: 0, y: 3, r: 2 }, + 1, + true + ), + { x: 0, y: 0, r: 1 }, + 'should find at the right-hand side' + ) + + t.end() + }) + + ts.test('case: nested input circles', (t) => { + t.almostEqualSphere( + sphere2.tangentCircle( + { x: -2, y: -2, r: 2 }, + { x: -2, y: -2, r: 1 }, + 1 + ), + { x: 1, y: -2, r: 1 }, + 'should find tangent to the largest of the concentric, at right' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: -2, y: -2, r: 1 }, + { x: -2, y: -2, r: 2 }, + 1 + ), + { x: 1, y: -2, r: 1 }, + 'should find tangent to the largest of the concentric, at right, smallest given first' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: -2, r: 2 }, + { x: 0, y: -1, r: 1 }, + 2 + ), + { x: 0, y: 2, r: 2 }, + 'should find close to the smaller one, non-concentric' + ) + + t.end() + }) + + ts.test('case: partly overlapping input circles', (t) => { + const ABOUT05 = 0.527951395671098 + const ABOUT49 = 4.972048604328897 + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 0, r: 2 }, + { x: 1, y: 1, r: 1 }, + 3 + ), + { x: ABOUT49, y: ABOUT05, r: 3 }, + 'should find left-hand tangent to both, wrt largest' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 1, y: 1, r: 1 }, + { x: 0, y: 0, r: 2 }, + 3 + ), + { x: ABOUT05, y: ABOUT49, r: 3 }, + 'should find left-hand tangent to both, wrt smallest' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 0, r: 2 }, + { x: 1, y: 1, r: 1 }, + 3, + true + ), + { x: ABOUT05, y: ABOUT49, r: 3 }, + 'should find right-hand tangent to both, wrt largest' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 1, y: 1, r: 1 }, + { x: 0, y: 0, r: 2 }, + 3, + true + ), + { x: ABOUT49, y: ABOUT05, r: 3 }, + 'should find right-hand tangent to both, wrt smallest' + ) + + t.end() + }) + + ts.test('case: distant input circles', (t) => { + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 0, r: 2 }, + { x: 0, y: 10, r: 1 }, + 2 + ), + { x: 0, y: 4, r: 2 }, + 'should find tangent to first' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 10, r: 1 }, + { x: 0, y: 0, r: 2 }, + 2 + ), + { x: 0, y: 7, r: 2 }, + 'should find tangent to first in spite of being smaller' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 0, r: 2 }, + { x: 0, y: 10, r: 1 }, + 2, + true + ), + { x: 0, y: 7, r: 2 }, + 'should find tangent to second' + ) + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 10, r: 1 }, + { x: 0, y: 0, r: 2 }, + 2, + true + ), + { x: 0, y: 4, r: 2 }, + 'should find tangent to second in spite of being smaller' + ) + + t.end() + }) + + ts.test('case: zero radius circles', (t) => { + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 0, y: 0, r: 0 }, + { x: 0, y: 0, r: 0 }, + 0 + ), + { x: 0, y: 0, r: 0 }, + 'should find trivial zero circle' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 2, y: 0, r: 0 }, + { x: 0, y: 0, r: 0 }, + 1 + ), + { x: 1, y: 0, r: 1 }, + 'should find unit circle between' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 2, y: 0, r: 1 }, + { x: 0, y: 0, r: 1 }, + 0 + ), + { x: 1, y: 0, r: 0 }, + 'should find zero circle between' + ) + + t.almostEqualSphere( + sphere2.tangentCircle( + { x: 2, y: 0, r: 0 }, + { x: 0, y: 0, r: 2 }, + 0 + ), + { x: 2, y: 0, r: 0 }, + 'should find zero circle at the surface' + ) + + t.end() + }) +} From 1f2f4de8cf9530a50e9ddc71d59d62d06aae738b Mon Sep 17 00:00:00 2001 From: Akseli Palen Date: Sun, 7 Jul 2024 19:29:11 +0300 Subject: [PATCH 2/5] implement sphere2.tangentCircles --- lib/sphere2/index.js | 1 + lib/sphere2/tangentCircles.js | 176 ++++++++++++++++++++++++++++ test/sphere2/index.test.js | 1 + test/sphere2/tangentCircles.test.js | 139 ++++++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 lib/sphere2/tangentCircles.js create mode 100644 test/sphere2/tangentCircles.test.js diff --git a/lib/sphere2/index.js b/lib/sphere2/index.js index 33c27da..9e5890c 100644 --- a/lib/sphere2/index.js +++ b/lib/sphere2/index.js @@ -37,6 +37,7 @@ exports.rotateBy = require('./rotateBy') exports.scaleBy = exports.homothety exports.size = require('./size') exports.tangentCircle = require('./tangentCircle') +exports.tangentCircles = require('./tangentCircles') exports.transitFrom = require('./transitFrom') exports.transitTo = require('./transitTo') exports.translate = require('./translate') diff --git a/lib/sphere2/tangentCircles.js b/lib/sphere2/tangentCircles.js new file mode 100644 index 0000000..ad2e6d2 --- /dev/null +++ b/lib/sphere2/tangentCircles.js @@ -0,0 +1,176 @@ +module.exports = (ca, cb, r) => { + // @affineplane.sphere2.tangentCircles(ca, cb, r) + // + // Find circles of radius r that are externally tangent to the given circles A and B. + // Usually there are two such circles, one at the left-hand side and one at the right-hand side + // with respect to the vector from A to B. The function returns the two circles in this left-right order. + // + // If the gap between A and B is too large for the circles to be tangent to both A and B + // then the function compromises and returns two circles: the first is tangent to A at the direction of B + // and the second is tangent to B at the direction of A. + // + // If the gap between A and B just fits a circle of radius r, the function returns only one circle. + // + // If the circles A and B are nested, the function compromises and returns only one circle that is externally + // tangent to the larger one so that the returned circle is still as close to the smaller one as possible. + // + // Parameters: + // ca + // a circle {x,y,r}, the circle A. + // cb + // a circle {x,y,r}, the circle B. + // r + // a number, the radius of the circles to find. + // + // Returns: + // an array of sphere2. The array contains either one or two sphere2. + // + // See also sphere2.tangentCircle if you only need either the left-hand or right-hand result. + // + + // Sort circles largest first. This avoids special treatment of cases + // where the point H (the point C projected on the AB line) is outside AB. + // However, we need to maintain handedness with respect to the original argument order. + let swapped = false + if (ca.r < cb.r) { + // Swap + const ct = cb + cb = ca + ca = ct + swapped = true + } + + // Vector from A to B, of length c. + const vab = { + x: cb.x - ca.x, + y: cb.y - ca.y + } + // Triangle sides + const a = cb.r + r // length of BC + const b = ca.r + r // length of AC + const c = Math.sqrt(vab.x * vab.x + vab.y * vab.y) // ca.r + cb.r for tangent A and B + + // Check special cases. + + const epsilon = 1000 * Number.EPSILON + const cUpper = c + epsilon + const cLower = c - epsilon + if (cUpper > 0 && cLower < 0) { + // The circles A and B are concentric i.e. the distance between their centers is zero. + // The circle C can be tangent to both only if A and B have equal radius. + // The best compromise is to let C be tangent to the one with the largest radius (A). + // The tangent position for the circle C is found at an arbitrary angle (choose 0 deg). + return [{ + x: ca.x + b, // because ca.r is always >= cb.r and b = ca.r + r. + y: ca.y, + r + }] + } + + if (c + cb.r <= ca.r) { + // The circle A fully covers the circle B. + // In other words, the circle B is completely inside the circle A. + // The best compromise is to let the circle C be tangent to the circle A + // at a position closest to the circle B. + const vac = { + x: vab.x * b / c, + y: vab.y * b / c + } + // The center point of the circle C: vc = va + vac + return [{ + x: ca.x + vac.x, + y: ca.y + vac.y, + r + }] + } + + if (a + b < cUpper && cLower < a + b) { + // There is a gap between the circles A and B that just fits the circle of radius r. + // In this case the function returns only one circle. Compute as tangent to the circle A. + // Vector from A to C. + const vac = { + x: vab.x * b / c, + y: vab.y * b / c + } + // The center point of the circle C: vc = va + vac + const cc = { + x: ca.x + vac.x, + y: ca.y + vac.y, + r + } + return [cc] + } + + if (a + b <= c) { + // The circles A and B are so far away that the circle C cannot connect them. + // Find a circle that is tangent with A and has center point along AB. + // Also, find a circle that is tangent with B and has center point along BA. + + // Find a circle C that is tangent to the circle A. + const vac = { + x: vab.x * b / c, + y: vab.y * b / c + } + // The center point of the circle C: vc = va + vac + const cc = { + x: ca.x + vac.x, + y: ca.y + vac.y, + r + } + // Find a circle D that is tangent to the circle B. + const vbc = { + x: -vab.x * a / c, + y: -vab.y * a / c + } + // The center point of the circle D: vd = vb + vbc + const cd = { + x: cb.x + vbc.x, + y: cb.y + vbc.y, + r + } + // Respect the original argument order regardless of their radii. + if (swapped) { + // Swapped order. Prefer the circle B. + return [cd, cc] + } + // Original order. + return [cc, cd] + } + + // After excluding the special cases above, + // it is now possible to find two circles of radius r that are + // tangent to both circles A and B. + const p = (a + b + c) / 2 + const A = Math.sqrt(p * (p - a) * (p - b) * (p - c)) + const h = 2 * A / c + const w = Math.sqrt(b * b - h * h) + // Vector from A to H, of length w. + const vw = { + x: vab.x * w / c, + y: vab.y * w / c + } + // Find the perpendicular vector from H to C, of length h. + const vh = { + x: -vw.y * h / w, + y: vw.x * h / w + } + // Find the center points for both left-hand and right-hand tangent circles. + // A + vw ± vh + const vleft = { + x: ca.x + vw.x - vh.x, + y: ca.y + vw.y - vh.y, + r + } + const vright = { + x: ca.x + vw.x + vh.x, + y: ca.y + vw.y + vh.y, + r + } + // Respect the original argument order regardless of their radii. + if (swapped) { + // Swapped order, the right is actually the left. + return [vright, vleft] + } + // Original order. + return [vleft, vright] +} diff --git a/test/sphere2/index.test.js b/test/sphere2/index.test.js index 3615a10..406e7c2 100644 --- a/test/sphere2/index.test.js +++ b/test/sphere2/index.test.js @@ -15,6 +15,7 @@ const units = { rotateBy: require('./rotateBy.test'), size: require('./size.test'), tangentCircle: require('./tangentCircle.test'), + tangentCircles: require('./tangentCircles.test'), transitFrom: require('./transitFrom.test'), transitTo: require('./transitTo.test'), validate: require('./validate.test') diff --git a/test/sphere2/tangentCircles.test.js b/test/sphere2/tangentCircles.test.js new file mode 100644 index 0000000..7545f34 --- /dev/null +++ b/test/sphere2/tangentCircles.test.js @@ -0,0 +1,139 @@ +const sphere2 = require('../../lib/sphere2') + +module.exports = (ts) => { + ts.test('case: basic tangent circles', (t) => { + let cs + + cs = sphere2.tangentCircles( + { x: 0, y: 0, r: 1 }, + { x: 2, y: 2, r: 1 }, + 1 + ) + t.almostEqualSphere(cs[0], { x: 2, y: 0, r: 1 }, 'should find left-hand circle') + t.almostEqualSphere(cs[1], { x: 0, y: 2, r: 1 }, 'should find right-hand circle') + + cs = sphere2.tangentCircles( + { x: 2, y: 2, r: 1 }, + { x: 0, y: 0, r: 1 }, + 1 + ) + t.almostEqualSphere(cs[0], { x: 0, y: 2, r: 1 }, 'should find left-hand circle') + t.almostEqualSphere(cs[1], { x: 2, y: 0, r: 1 }, 'should find right-hand circle') + + t.end() + }) + + ts.test('case: nested input circles', (t) => { + // concentric + const csa = sphere2.tangentCircles( + { x: -2, y: -2, r: 2 }, + { x: -2, y: -2, r: 1 }, + 1 + ) + t.equals(csa.length, 1, 'should find only one circle') + t.almostEqualSphere( + csa[0], + { x: 1, y: -2, r: 1 }, + 'should find tangent to the largest of the concentric, at right' + ) + + const csb = sphere2.tangentCircles( + { x: -2, y: -2, r: 1 }, + { x: -2, y: -2, r: 2 }, + 1 + ) + t.equals(csb.length, 1, 'should find only one circle, regardless of the smallest given first') + t.almostEqualSphere(csb[0], csa[0], 'should find the same circle') + + // non-concentric + const csc = sphere2.tangentCircles( + { x: 0, y: -2, r: 2 }, + { x: 0, y: -1, r: 1 }, + 2 + ) + t.equals(csc.length, 1, 'should find only one circle, regardless of the input circles are non-concentric') + t.almostEqualSphere( + csc[0], + { x: 0, y: 2, r: 2 }, + 'should find a circle tangent to the first, close to the second' + ) + + t.end() + }) + + ts.test('case: partly overlapping input circles', (t) => { + const ABOUT05 = 0.527951395671098 + const ABOUT49 = 4.972048604328897 + + const csa = sphere2.tangentCircles( + { x: 0, y: 0, r: 2 }, + { x: 1, y: 1, r: 1 }, + 3 + ) + t.almostEqualSphere(csa[0], { x: ABOUT49, y: ABOUT05, r: 3 }, 'should find left-hand tangent') + t.almostEqualSphere(csa[1], { x: ABOUT05, y: ABOUT49, r: 3 }, 'should find right-hand tangent') + + // opposite order + const csb = sphere2.tangentCircles( + { x: 1, y: 1, r: 1 }, + { x: 0, y: 0, r: 2 }, + 3 + ) + t.almostEqualSphere(csb[0], { x: ABOUT05, y: ABOUT49, r: 3 }, 'should find left-hand tangent') + t.almostEqualSphere(csb[1], { x: ABOUT49, y: ABOUT05, r: 3 }, 'should find right-hand tangent') + + t.end() + }) + + ts.test('case: distant input circles', (t) => { + const csa = sphere2.tangentCircles( + { x: 0, y: 0, r: 2 }, + { x: 0, y: 10, r: 1 }, + 2 + ) + t.almostEqualSphere(csa[0], { x: 0, y: 4, r: 2 }, 'should find tangent to first') + t.almostEqualSphere(csa[1], { x: 0, y: 7, r: 2 }, 'should find tangent to second') + + const csb = sphere2.tangentCircles( + { x: 0, y: 10, r: 1 }, + { x: 0, y: 0, r: 2 }, + 2 + ) + t.almostEqualSphere(csb[0], { x: 0, y: 7, r: 2 }, 'should find tangent to first') + t.almostEqualSphere(csb[1], { x: 0, y: 4, r: 2 }, 'should find tangent to second') + + t.end() + }) + + ts.test('case: zero radius circles', (t) => { + const csa = sphere2.tangentCircles( + { x: 0, y: 0, r: 0 }, + { x: 0, y: 0, r: 0 }, + 0 + ) + t.deepEqual(csa, [{ x: 0, y: 0, r: 0 }], 'should find one zero circle') + + const csb = sphere2.tangentCircles( + { x: 2, y: 0, r: 0 }, + { x: 0, y: 0, r: 0 }, + 1 + ) + t.deepEqual(csb, [{ x: 1, y: 0, r: 1 }], 'should find only one unit circle between') + + const csc = sphere2.tangentCircles( + { x: 2, y: 0, r: 1 }, + { x: 0, y: 0, r: 1 }, + 0 + ) + t.deepEqual(csc, [{ x: 1, y: 0, r: 0 }], 'should find only one zero circle between') + + const csd = sphere2.tangentCircles( + { x: 2, y: 0, r: 0 }, + { x: 0, y: 0, r: 2 }, + 0 + ) + t.deepEqual(csd, [{ x: 2, y: 0, r: 0 }], 'should find only one zero circle at the surface') + + t.end() + }) +} From 93a562cafa68d6704a2bbd35c4c5c670497f2972 Mon Sep 17 00:00:00 2001 From: Akseli Palen Date: Sun, 7 Jul 2024 21:07:13 +0300 Subject: [PATCH 3/5] render docs --- docs/API.md | 149 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 40 deletions(-) diff --git a/docs/API.md b/docs/API.md index 1ef3551..d1d1d9f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -9934,6 +9934,8 @@ Aliases: [affineplane.circle2](#affineplanecircle2) - [affineplane.sphere2.rotateBy](#affineplanesphere2rotateby) - [affineplane.sphere2.scaleBy](#affineplanesphere2scaleby) - [affineplane.sphere2.size](#affineplanesphere2size) +- [affineplane.sphere2.tangentCircle](#affineplanesphere2tangentcircle) +- [affineplane.sphere2.tangentCircles](#affineplanesphere2tangentcircles) - [affineplane.sphere2.transitFrom](#affineplanesphere2transitfrom) - [affineplane.sphere2.transitTo](#affineplanesphere2transitto) - [affineplane.sphere2.translate](#affineplanesphere2translate) @@ -10310,6 +10312,73 @@ Get the rectangular size of the circle. Source: [size.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere2/size.js) + +## [affineplane](#affineplane).[sphere2](#affineplanesphere2).[tangentCircle](#affineplanesphere2tangentcircle)(ca, cb, r, righthand) + +Find a circle C that is left-hand tangent to the circles A and B. +Use the fourth parameter to switch to right-hand tangent. +Note the coordinate system: x-axis points right and y-axis points down. + +If the gap between A and B is too large for C to be tangent to both +then the resulting circle is tangent with A and as close to B as possible. +Under the righthand flag, the preference is reversed. + +

Parameters:

+ +- *ca* + - a [sphere2](#affineplanesphere2) {x,y,r}, the circle A. +- *cb* + - a [sphere2](#affineplanesphere2) {x,y,r}, the circle B. +- *r* + - a number, a [dist2](#affineplanedist2), the radius of the circle C. +- *righthand* + - a boolean, default false. Set true to find right-hand tangent circle. + + +

Returns:

+ +- a [sphere2](#affineplanesphere2) `{x,y,r}` + + +See also [sphere2](#affineplanesphere2).tangentCircles for efficient computation of both hands with just one function call. + +Source: [tangentCircle.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere2/tangentCircle.js) + + +## [affineplane](#affineplane).[sphere2](#affineplanesphere2).[tangentCircles](#affineplanesphere2tangentcircles)(ca, cb, r) + +Find circles of radius r that are externally tangent to the given circles A and B. +Usually there are two such circles, one at the left-hand side and one at the right-hand side +with respect to the vector from A to B. The function returns the two circles in this left-right order. + +If the gap between A and B is too large for the circles to be tangent to both A and B +then the function compromises and returns two circles: the first is tangent to A at the direction of B +and the second is tangent to B at the direction of A. + +If the gap between A and B just fits a circle of radius r, the function returns only one circle. + +If the circles A and B are nested, the function compromises and returns only one circle that is externally +tangent to the larger one so that the returned circle is still as close to the smaller one as possible. + +

Parameters:

+ +- *ca* + - a circle {x,y,r}, the circle A. +- *cb* + - a circle {x,y,r}, the circle B. +- *r* + - a number, the radius of the circles to find. + + +

Returns:

+ +- an array of [sphere2](#affineplanesphere2). The array contains either one or two [sphere2](#affineplanesphere2). + + +See also [sphere2](#affineplanesphere2).tangentCircle if you only need either the left-hand or right-hand result. + +Source: [tangentCircles.js](https://github.com/axelpale/affineplane/blob/main/lib/sphere2/tangentCircles.js) + ## [affineplane](#affineplane).[sphere2](#affineplanesphere2).[transitFrom](#affineplanesphere2transitfrom)(sphere, source) @@ -11640,6 +11709,46 @@ Translation of the plane does not affect the vector. Source: [transitTo.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/transitTo.js) + +## [affineplane](#affineplane).[vec2](#affineplanevec2).[unit](#affineplanevec2unit)(v) + +Get unit vector parallel to the given vector. +The magnitude of unit vector is equal to one. +If zero vector is given, assume direction towards positive x. + +

Parameters:

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

Returns:

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

Parameters:

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

Returns:

+ +- a boolean + + +Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/validate.js) + ## [affineplane](#affineplane).[vec3](#affineplanevec3) @@ -11709,46 +11818,6 @@ The zero vector in 4D Source: [vec4/index.js](https://github.com/axelpale/affineplane/blob/main/lib/vec4/index.js) - -## [affineplane](#affineplane).[vec2](#affineplanevec2).[unit](#affineplanevec2unit)(v) - -Get unit vector parallel to the given vector. -The magnitude of unit vector is equal to one. -If zero vector is given, assume direction towards positive x. - -

Parameters:

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

Returns:

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

Parameters:

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

Returns:

- -- a boolean - - -Source: [validate.js](https://github.com/axelpale/affineplane/blob/main/lib/vec2/validate.js) - ## [affineplane](#affineplane).[vec3](#affineplanevec3).[add](#affineplanevec3add)(v, w) From 4adb2868d99a1191bb575d4f1e58c1a724a57f78 Mon Sep 17 00:00:00 2001 From: Akseli Palen Date: Sun, 7 Jul 2024 21:07:32 +0300 Subject: [PATCH 4/5] upgrade dev dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 918ef62..fa93fbb 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "license": "MIT", "devDependencies": { "async": "^3.2.5", - "genversion": "^3.1.1", - "nodemon": "^3.0.2", + "genversion": "^3.2.0", + "nodemon": "^3.1.4", "standard": "^17.1.0", "tap-arc": "^1.2.2", - "tape": "^5.7.2", + "tape": "^5.8.1", "yamdog": "^2.1.0" }, "scripts": { From c01c35175707b1a43e339baed5ccfa6c2a1f6f62 Mon Sep 17 00:00:00 2001 From: Akseli Palen Date: Sun, 7 Jul 2024 21:14:07 +0300 Subject: [PATCH 5/5] bump v2.19.0 --- docs/API.md | 2 +- lib/version.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index d1d1d9f..6f511bf 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,5 +1,5 @@ -# Affineplane API Documentation v2.18.0 +# Affineplane API Documentation v2.19.0 Welcome to affineplane API reference documentation. These docs are generated with [yamdog](https://axelpale.github.io/yamdog/). diff --git a/lib/version.js b/lib/version.js index 0a37470..98ba5b2 100644 --- a/lib/version.js +++ b/lib/version.js @@ -1,2 +1,2 @@ // Generated by genversion. -module.exports = '2.18.0' +module.exports = '2.19.0' diff --git a/package.json b/package.json index fa93fbb..20a28a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "affineplane", - "version": "2.18.0", + "version": "2.19.0", "description": "Affine geometry library for 2D and 3D spaces", "keywords": [ "affine",