Files
to_docx/client/node_modules/cytoscape/src/math.mjs
T
Chen Xiao 0b64e2de94 First commit.
Signed-off-by: Chen Xiao <abigwc@gmail.com>
2026-05-08 14:43:16 +08:00

1479 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const arePositionsSame = ( p1, p2 ) =>
p1.x === p2.x && p1.y === p2.y;
export const copyPosition = p =>
({ x: p.x, y: p.y });
export const modelToRenderedPosition = ( p, zoom, pan ) => ({
x: p.x * zoom + pan.x,
y: p.y * zoom + pan.y
});
export const renderedToModelPosition = ( p, zoom, pan ) => ({
x: ( p.x - pan.x ) / zoom,
y: ( p.y - pan.y ) / zoom
});
export const array2point = arr => ({
x: arr[0],
y: arr[1]
});
export const min = ( arr, begin = 0, end = arr.length ) => {
let min = Infinity;
for( let i = begin; i < end; i++ ){
let val = arr[i];
if( isFinite(val) ){
min = Math.min( val, min );
}
}
return min;
};
export const max = ( arr, begin = 0, end = arr.length ) => {
let max = -Infinity;
for( let i = begin; i < end; i++ ){
let val = arr[i];
if( isFinite(val) ){
max = Math.max( val, max );
}
}
return max;
};
export const mean = ( arr, begin = 0, end = arr.length ) => {
let total = 0;
let n = 0;
for( let i = begin; i < end; i++ ){
let val = arr[i];
if( isFinite(val) ){
total += val;
n++;
}
}
return total / n;
};
export const median = ( arr, begin = 0, end = arr.length, copy = true, sort = true, includeHoles = true ) => {
if( copy ){
arr = arr.slice( begin, end );
} else {
if( end < arr.length ){
arr.splice( end, arr.length - end );
}
if( begin > 0 ){
arr.splice( 0, begin );
}
}
// all non finite (e.g. Infinity, NaN) elements must be -Infinity so they go to the start
let off = 0; // offset from non-finite values
for( let i = arr.length - 1; i >= 0; i-- ){
let v = arr[i];
if( includeHoles ){
if( !isFinite(v) ){
arr[i] = -Infinity;
off++;
}
} else { // just remove it if we don't want to consider holes
arr.splice(i, 1);
}
}
if( sort ){
arr.sort( (a, b) => a - b ); // requires copy = true if you don't want to change the orig
}
let len = arr.length;
let mid = Math.floor( len / 2 );
if( len % 2 !== 0 ){
return arr[mid + 1 + off];
} else {
return ( arr[mid - 1 + off] + arr[mid + off] )/2;
}
};
export const deg2rad = deg =>
Math.PI * deg / 180;
export const getAngleFromDisp = ( dispX, dispY ) =>
Math.atan2( dispY, dispX ) - Math.PI / 2;
export const log2 = Math.log2 || (n => Math.log( n ) / Math.log( 2 ));
export const signum = x => {
if( x > 0 ){
return 1;
} else if( x < 0 ){
return -1;
} else {
return 0;
}
};
export const dist = ( p1, p2 ) =>
Math.sqrt( sqdist( p1, p2 ) );
export const sqdist = ( p1, p2 ) => {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return dx * dx + dy * dy;
};
export const inPlaceSumNormalize = v => {
let length = v.length;
// First, get sum of all elements
let total = 0;
for( let i = 0; i < length; i++ ){
total += v[i];
}
// Now, divide each by the sum of all elements
for( let i = 0; i < length; i++ ){
v[i] = v[i] / total;
}
return v;
};
export const normalize = v => inPlaceSumNormalize( v.slice() );
// from http://en.wikipedia.org/wiki/Bézier_curve#Quadratic_curves
export const qbezierAt = ( p0, p1, p2, t ) =>
(1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
export const qbezierPtAt = ( p0, p1, p2, t ) => ({
x: qbezierAt( p0.x, p1.x, p2.x, t ),
y: qbezierAt( p0.y, p1.y, p2.y, t )
});
export const lineAt = ( p0, p1, t, d ) => {
let vec = {
x: p1.x - p0.x,
y: p1.y - p0.y
};
let vecDist = dist( p0, p1 );
let normVec = {
x: vec.x / vecDist,
y: vec.y / vecDist
};
t = t == null ? 0 : t;
d = d != null ? d : t * vecDist;
return {
x: p0.x + normVec.x * d,
y: p0.y + normVec.y * d
};
};
export const lineAtDist = ( p0, p1, d ) =>
lineAt( p0, p1, undefined, d );
// get angle at A via cosine law
export const triangleAngle = ( A, B, C ) => {
let a = dist( B, C );
let b = dist( A, C );
let c = dist( A, B );
return Math.acos( (a*a + b*b - c*c)/(2*a*b) );
};
export const bound = ( min, val, max ) =>
Math.max( min, Math.min( max, val ) );
// makes a full bb (x1, y1, x2, y2, w, h) from implicit params
export const makeBoundingBox = bb => {
if( bb == null ){
return {
x1: Infinity,
y1: Infinity,
x2: -Infinity,
y2: -Infinity,
w: 0,
h: 0
};
} else if( bb.x1 != null && bb.y1 != null ){
if( bb.x2 != null && bb.y2 != null && bb.x2 >= bb.x1 && bb.y2 >= bb.y1 ){
return {
x1: bb.x1,
y1: bb.y1,
x2: bb.x2,
y2: bb.y2,
w: bb.x2 - bb.x1,
h: bb.y2 - bb.y1
};
} else if( bb.w != null && bb.h != null && bb.w >= 0 && bb.h >= 0 ){
return {
x1: bb.x1,
y1: bb.y1,
x2: bb.x1 + bb.w,
y2: bb.y1 + bb.h,
w: bb.w,
h: bb.h
};
}
}
};
export const copyBoundingBox = bb => {
return { x1: bb.x1, x2: bb.x2, w: bb.w, y1: bb.y1, y2: bb.y2, h: bb.h };
};
export const clearBoundingBox = bb => {
bb.x1 = Infinity;
bb.y1 = Infinity;
bb.x2 = -Infinity;
bb.y2 = -Infinity;
bb.w = 0;
bb.h = 0;
};
export const shiftBoundingBox = function( bb, dx, dy ){
return {
x1: bb.x1 + dx,
x2: bb.x2 + dx,
y1: bb.y1 + dy,
y2: bb.y2 + dy,
w: bb.w,
h: bb.h
};
};
export const updateBoundingBox = function( bb1, bb2 ){
// update bb1 with bb2 bounds
bb1.x1 = Math.min( bb1.x1, bb2.x1 );
bb1.x2 = Math.max( bb1.x2, bb2.x2 );
bb1.w = bb1.x2 - bb1.x1;
bb1.y1 = Math.min( bb1.y1, bb2.y1 );
bb1.y2 = Math.max( bb1.y2, bb2.y2 );
bb1.h = bb1.y2 - bb1.y1;
};
export const expandBoundingBoxByPoint = ( bb, x, y ) => {
bb.x1 = Math.min( bb.x1, x );
bb.x2 = Math.max( bb.x2, x );
bb.w = bb.x2 - bb.x1;
bb.y1 = Math.min( bb.y1, y );
bb.y2 = Math.max( bb.y2, y );
bb.h = bb.y2 - bb.y1;
};
export const expandBoundingBox = ( bb, padding = 0 ) => {
bb.x1 -= padding;
bb.x2 += padding;
bb.y1 -= padding;
bb.y2 += padding;
bb.w = bb.x2 - bb.x1;
bb.h = bb.y2 - bb.y1;
return bb;
};
export const expandBoundingBoxSides = (bb, padding = [0] ) => {
let top, right, bottom, left;
if (padding.length === 1) {
top = right = bottom = left = padding[0];
} else if (padding.length === 2) {
top = bottom = padding[0];
left = right = padding[1];
} else if (padding.length === 4) {
[top, right, bottom, left] = padding;
}
bb.x1 -= left;
bb.x2 += right;
bb.y1 -= top;
bb.y2 += bottom;
bb.w = bb.x2 - bb.x1;
bb.h = bb.y2 - bb.y1;
return bb;
};
const expandToInt = x => x > 0 ? Math.ceil(x) : Math.floor(x);
export const expandBoundingBoxToInts = ( bb, padding = 0 ) => {
bb.x1 = expandToInt(bb.x1 - padding);
bb.y1 = expandToInt(bb.y1 - padding);
bb.x2 = expandToInt(bb.x2 + padding);
bb.y2 = expandToInt(bb.y2 + padding);
bb.w = bb.x2 - bb.x1;
bb.h = bb.y2 - bb.y1;
};
// assign the values of bb2 into bb1
export const assignBoundingBox = ( bb1, bb2 ) => {
bb1.x1 = bb2.x1;
bb1.y1 = bb2.y1;
bb1.x2 = bb2.x2;
bb1.y2 = bb2.y2;
bb1.w = bb1.x2 - bb1.x1;
bb1.h = bb1.y2 - bb1.y1;
};
export const assignShiftToBoundingBox = ( bb, delta ) => {
bb.x1 += delta.x;
bb.x2 += delta.x;
bb.y1 += delta.y;
bb.y2 += delta.y;
};
export const boundingBoxesIntersect = ( bb1, bb2 ) => {
// case: one bb to right of other
if( bb1.x1 > bb2.x2 ){ return false; }
if( bb2.x1 > bb1.x2 ){ return false; }
// case: one bb to left of other
if( bb1.x2 < bb2.x1 ){ return false; }
if( bb2.x2 < bb1.x1 ){ return false; }
// case: one bb above other
if( bb1.y2 < bb2.y1 ){ return false; }
if( bb2.y2 < bb1.y1 ){ return false; }
// case: one bb below other
if( bb1.y1 > bb2.y2 ){ return false; }
if( bb2.y1 > bb1.y2 ){ return false; }
// otherwise, must have some overlap
return true;
};
export const inBoundingBox = ( bb, x, y ) =>
bb.x1 <= x && x <= bb.x2 && bb.y1 <= y && y <= bb.y2;
export const pointInBoundingBox = ( bb, pt ) =>
inBoundingBox( bb, pt.x, pt.y );
export const boundingBoxInBoundingBox = ( bb1, bb2 ) => (
inBoundingBox( bb1, bb2.x1, bb2.y1 )
&& inBoundingBox( bb1, bb2.x2, bb2.y2 )
);
export const hypot = Math.hypot ?? ((x, y) => Math.sqrt(x * x + y * y));
function inflatePolygon(polygon, d) {
if (polygon.length < 3) {
throw new Error('Need at least 3 vertices');
}
// Helpers
const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
const scale = (v, s) => ({ x: v.x * s, y: v.y * s });
const cross = (u, v) => u.x * v.y - u.y * v.x;
const normalize = v => {
const len = hypot(v.x, v.y);
return len === 0 ? { x: 0, y: 0 } : { x: v.x / len, y: v.y / len };
};
// Signed area (positive = CCW)
const signedArea = pts => {
let A = 0;
for (let i = 0; i < pts.length; i++) {
const p = pts[i], q = pts[(i + 1) % pts.length];
A += p.x * q.y - q.x * p.y;
}
return A / 2;
};
// Lineline intersection (infinite lines)
const intersectLines = (p1, p2, p3, p4) => {
const r = sub(p2, p1);
const s = sub(p4, p3);
const denom = cross(r, s);
if (Math.abs(denom) < 1e-9) {
// Parallel or nearly so — fallback to midpoint
return add(p1, scale(r, 0.5));
}
const t = cross(sub(p3, p1), s) / denom;
return add(p1, scale(r, t));
};
// Make a shallow copy and enforce CCW
const pts = polygon.map(p => ({ x: p.x, y: p.y }));
if (signedArea(pts) < 0) pts.reverse();
const n = pts.length;
// Compute outward normals for each edge
const normals = [];
for (let i = 0; i < n; i++) {
const p = pts[i], q = pts[(i + 1) % n];
const edge = sub(q, p);
// For CCW polygon, inward normal = (-edge.y, edge.x)
// so outward normal = (edge.y, -edge.x)
const out = normalize({ x: edge.y, y: -edge.x });
normals.push(out);
}
// Build offset edges
const offsetEdges = normals.map((nrm, i) => {
const p1 = add(pts[i], scale(nrm, d));
const p2 = add(pts[(i + 1) % n], scale(nrm, d));
return { p1, p2 };
});
// Intersect consecutive offset edges
const inflated = [];
for (let i = 0; i < n; i++) {
const prevEdge = offsetEdges[(i - 1 + n) % n];
const currEdge = offsetEdges[i];
const ip = intersectLines(prevEdge.p1, prevEdge.p2, currEdge.p1, currEdge.p2);
inflated.push(ip);
}
return inflated;
}
export function miterBox(pts, centerX, centerY, width, height, strokeWidth) {
const tpts = transformPoints(pts, centerX, centerY, width, height);
let offsetPoints = inflatePolygon(tpts, strokeWidth);
let bb = makeBoundingBox();
offsetPoints.forEach(pt => expandBoundingBoxByPoint(bb, pt.x, pt.y));
return bb;
}
export const roundRectangleIntersectLine = ( x, y, nodeX, nodeY, width, height, padding, radius = 'auto' ) => {
let cornerRadius = radius === 'auto' ? getRoundRectangleRadius( width, height ) : radius;
let halfWidth = width / 2;
let halfHeight = height / 2;
cornerRadius = Math.min(cornerRadius, halfWidth, halfHeight);
const doWidth = cornerRadius !== halfWidth, doHeight = cornerRadius !== halfHeight;
// Check intersections with straight line segments
let straightLineIntersections;
// Top segment, left to right
if( doWidth ){
let topStartX = nodeX - halfWidth + cornerRadius - padding;
let topStartY = nodeY - halfHeight - padding;
let topEndX = nodeX + halfWidth - cornerRadius + padding;
let topEndY = topStartY;
straightLineIntersections = finiteLinesIntersect(
x, y, nodeX, nodeY, topStartX, topStartY, topEndX, topEndY, false );
if( straightLineIntersections.length > 0 ){
return straightLineIntersections;
}
}
// Right segment, top to bottom
if( doHeight ){
let rightStartX = nodeX + halfWidth + padding;
let rightStartY = nodeY - halfHeight + cornerRadius - padding;
let rightEndX = rightStartX;
let rightEndY = nodeY + halfHeight - cornerRadius + padding;
straightLineIntersections = finiteLinesIntersect(
x, y, nodeX, nodeY, rightStartX, rightStartY, rightEndX, rightEndY, false );
if( straightLineIntersections.length > 0 ){
return straightLineIntersections;
}
}
// Bottom segment, left to right
if( doWidth ){
let bottomStartX = nodeX - halfWidth + cornerRadius - padding;
let bottomStartY = nodeY + halfHeight + padding;
let bottomEndX = nodeX + halfWidth - cornerRadius + padding;
let bottomEndY = bottomStartY;
straightLineIntersections = finiteLinesIntersect(
x, y, nodeX, nodeY, bottomStartX, bottomStartY, bottomEndX, bottomEndY, false );
if( straightLineIntersections.length > 0 ){
return straightLineIntersections;
}
}
// Left segment, top to bottom
if( doHeight ){
let leftStartX = nodeX - halfWidth - padding;
let leftStartY = nodeY - halfHeight + cornerRadius - padding;
let leftEndX = leftStartX;
let leftEndY = nodeY + halfHeight - cornerRadius + padding;
straightLineIntersections = finiteLinesIntersect(
x, y, nodeX, nodeY, leftStartX, leftStartY, leftEndX, leftEndY, false );
if( straightLineIntersections.length > 0 ){
return straightLineIntersections;
}
}
// Check intersections with arc segments
let arcIntersections;
// Top Left
{
let topLeftCenterX = nodeX - halfWidth + cornerRadius;
let topLeftCenterY = nodeY - halfHeight + cornerRadius;
arcIntersections = intersectLineCircle(
x, y, nodeX, nodeY,
topLeftCenterX, topLeftCenterY, cornerRadius + padding );
// Ensure the intersection is on the desired quarter of the circle
if( arcIntersections.length > 0
&& arcIntersections[0] <= topLeftCenterX
&& arcIntersections[1] <= topLeftCenterY ){
return [ arcIntersections[0], arcIntersections[1] ];
}
}
// Top Right
{
let topRightCenterX = nodeX + halfWidth - cornerRadius;
let topRightCenterY = nodeY - halfHeight + cornerRadius;
arcIntersections = intersectLineCircle(
x, y, nodeX, nodeY,
topRightCenterX, topRightCenterY, cornerRadius + padding );
// Ensure the intersection is on the desired quarter of the circle
if( arcIntersections.length > 0
&& arcIntersections[0] >= topRightCenterX
&& arcIntersections[1] <= topRightCenterY ){
return [ arcIntersections[0], arcIntersections[1] ];
}
}
// Bottom Right
{
let bottomRightCenterX = nodeX + halfWidth - cornerRadius;
let bottomRightCenterY = nodeY + halfHeight - cornerRadius;
arcIntersections = intersectLineCircle(
x, y, nodeX, nodeY,
bottomRightCenterX, bottomRightCenterY, cornerRadius + padding );
// Ensure the intersection is on the desired quarter of the circle
if( arcIntersections.length > 0
&& arcIntersections[0] >= bottomRightCenterX
&& arcIntersections[1] >= bottomRightCenterY ){
return [ arcIntersections[0], arcIntersections[1] ];
}
}
// Bottom Left
{
let bottomLeftCenterX = nodeX - halfWidth + cornerRadius;
let bottomLeftCenterY = nodeY + halfHeight - cornerRadius;
arcIntersections = intersectLineCircle(
x, y, nodeX, nodeY,
bottomLeftCenterX, bottomLeftCenterY, cornerRadius + padding );
// Ensure the intersection is on the desired quarter of the circle
if( arcIntersections.length > 0
&& arcIntersections[0] <= bottomLeftCenterX
&& arcIntersections[1] >= bottomLeftCenterY ){
return [ arcIntersections[0], arcIntersections[1] ];
}
}
return []; // if nothing
};
export const inLineVicinity = ( x, y, lx1, ly1, lx2, ly2, tolerance ) => {
let t = tolerance;
let x1 = Math.min( lx1, lx2 );
let x2 = Math.max( lx1, lx2 );
let y1 = Math.min( ly1, ly2 );
let y2 = Math.max( ly1, ly2 );
return x1 - t <= x && x <= x2 + t
&& y1 - t <= y && y <= y2 + t;
};
export const inBezierVicinity = ( x, y, x1, y1, x2, y2, x3, y3, tolerance ) => {
let bb = {
x1: Math.min( x1, x3, x2 ) - tolerance,
x2: Math.max( x1, x3, x2 ) + tolerance,
y1: Math.min( y1, y3, y2 ) - tolerance,
y2: Math.max( y1, y3, y2 ) + tolerance
};
// if outside the rough bounding box for the bezier, then it can't be a hit
if( x < bb.x1 || x > bb.x2 || y < bb.y1 || y > bb.y2 ){
// console.log('bezier out of rough bb')
return false;
} else {
// console.log('do more expensive check');
return true;
}
};
export const solveQuadratic = ( a, b, c, val ) => {
c -= val;
var r = b * b - 4 * a * c;
if( r < 0 ){ return []; }
var sqrtR = Math.sqrt( r );
var denom = 2 * a;
var root1 = ( -b + sqrtR ) / denom;
var root2 = ( -b - sqrtR ) / denom;
return [ root1, root2 ];
};
export const solveCubic = ( a, b, c, d, result ) => {
// Solves a cubic function, returns root in form [r1, i1, r2, i2, r3, i3], where
// r is the real component, i is the imaginary component
// An implementation of the Cardano method from the year 1545
// http://en.wikipedia.org/wiki/Cubic_function#The_nature_of_the_roots
var epsilon = 0.00001;
// avoid division by zero while keeping the overall expression close in value
if( a === 0 ){
a = epsilon;
}
b /= a;
c /= a;
d /= a;
let discriminant, q, r, dum1, s, t, term1, r13;
q = (3.0 * c - (b * b)) / 9.0;
r = -(27.0 * d) + b * (9.0 * c - 2.0 * (b * b));
r /= 54.0;
discriminant = q * q * q + r * r;
result[1] = 0;
term1 = (b / 3.0);
if( discriminant > 0 ){
s = r + Math.sqrt( discriminant );
s = ((s < 0) ? -Math.pow( -s, (1.0 / 3.0) ) : Math.pow( s, (1.0 / 3.0) ));
t = r - Math.sqrt( discriminant );
t = ((t < 0) ? -Math.pow( -t, (1.0 / 3.0) ) : Math.pow( t, (1.0 / 3.0) ));
result[0] = -term1 + s + t;
term1 += (s + t) / 2.0;
result[4] = result[2] = -term1;
term1 = Math.sqrt( 3.0 ) * (-t + s) / 2;
result[3] = term1;
result[5] = -term1;
return;
}
result[5] = result[3] = 0;
if( discriminant === 0 ){
r13 = ((r < 0) ? -Math.pow( -r, (1.0 / 3.0) ) : Math.pow( r, (1.0 / 3.0) ));
result[0] = -term1 + 2.0 * r13;
result[4] = result[2] = -(r13 + term1);
return;
}
q = -q;
dum1 = q * q * q;
dum1 = Math.acos( r / Math.sqrt( dum1 ) );
r13 = 2.0 * Math.sqrt( q );
result[0] = -term1 + r13 * Math.cos( dum1 / 3.0 );
result[2] = -term1 + r13 * Math.cos( (dum1 + 2.0 * Math.PI) / 3.0 );
result[4] = -term1 + r13 * Math.cos( (dum1 + 4.0 * Math.PI) / 3.0 );
return;
};
export const sqdistToQuadraticBezier = ( x, y, x1, y1, x2, y2, x3, y3 ) => {
// Find minimum distance by using the minimum of the distance
// function between the given point and the curve
// This gives the coefficients of the resulting cubic equation
// whose roots tell us where a possible minimum is
// (Coefficients are divided by 4)
let a = 1.0 * x1 * x1 - 4 * x1 * x2 + 2 * x1 * x3 + 4 * x2 * x2 - 4 * x2 * x3 + x3 * x3
+ y1 * y1 - 4 * y1 * y2 + 2 * y1 * y3 + 4 * y2 * y2 - 4 * y2 * y3 + y3 * y3;
let b = 1.0 * 9 * x1 * x2 - 3 * x1 * x1 - 3 * x1 * x3 - 6 * x2 * x2 + 3 * x2 * x3
+ 9 * y1 * y2 - 3 * y1 * y1 - 3 * y1 * y3 - 6 * y2 * y2 + 3 * y2 * y3;
let c = 1.0 * 3 * x1 * x1 - 6 * x1 * x2 + x1 * x3 - x1 * x + 2 * x2 * x2 + 2 * x2 * x - x3 * x
+ 3 * y1 * y1 - 6 * y1 * y2 + y1 * y3 - y1 * y + 2 * y2 * y2 + 2 * y2 * y - y3 * y;
let d = 1.0 * x1 * x2 - x1 * x1 + x1 * x - x2 * x
+ y1 * y2 - y1 * y1 + y1 * y - y2 * y;
// debug("coefficients: " + a / a + ", " + b / a + ", " + c / a + ", " + d / a);
let roots = [];
// Use the cubic solving algorithm
solveCubic( a, b, c, d, roots );
let zeroThreshold = 0.0000001;
let params = [];
for( let index = 0; index < 6; index += 2 ){
if( Math.abs( roots[ index + 1] ) < zeroThreshold
&& roots[ index ] >= 0
&& roots[ index ] <= 1.0 ){
params.push( roots[ index ] );
}
}
params.push( 1.0 );
params.push( 0.0 );
let minDistanceSquared = -1;
let curX, curY, distSquared;
for( let i = 0; i < params.length; i++ ){
curX = Math.pow( 1.0 - params[ i ], 2.0 ) * x1
+ 2.0 * (1 - params[ i ]) * params[ i ] * x2
+ params[ i ] * params[ i ] * x3;
curY = Math.pow( 1 - params[ i ], 2.0 ) * y1
+ 2 * (1.0 - params[ i ]) * params[ i ] * y2
+ params[ i ] * params[ i ] * y3;
distSquared = Math.pow( curX - x, 2 ) + Math.pow( curY - y, 2 );
// debug('distance for param ' + params[i] + ": " + Math.sqrt(distSquared));
if( minDistanceSquared >= 0 ){
if( distSquared < minDistanceSquared ){
minDistanceSquared = distSquared;
}
} else {
minDistanceSquared = distSquared;
}
}
return minDistanceSquared;
};
export const sqdistToFiniteLine = ( x, y, x1, y1, x2, y2 ) => {
let offset = [ x - x1, y - y1 ];
let line = [ x2 - x1, y2 - y1 ];
let lineSq = line[0] * line[0] + line[1] * line[1];
let hypSq = offset[0] * offset[0] + offset[1] * offset[1];
let dotProduct = offset[0] * line[0] + offset[1] * line[1];
let adjSq = dotProduct * dotProduct / lineSq;
if( dotProduct < 0 ){
return hypSq;
}
if( adjSq > lineSq ){
return (x - x2) * (x - x2) + (y - y2) * (y - y2);
}
return hypSq - adjSq;
};
export const pointInsidePolygonPoints = ( x, y, points ) => {
let x1, y1, x2, y2;
let y3;
// Intersect with vertical line through (x, y)
let up = 0;
// let down = 0;
for( let i = 0; i < points.length / 2; i++ ){
x1 = points[ i * 2];
y1 = points[ i * 2 + 1];
if( i + 1 < points.length / 2 ){
x2 = points[ (i + 1) * 2];
y2 = points[ (i + 1) * 2 + 1];
} else {
x2 = points[ (i + 1 - points.length / 2) * 2];
y2 = points[ (i + 1 - points.length / 2) * 2 + 1];
}
if( x1 == x && x2 == x ){
// then ignore
} else if( (x1 >= x && x >= x2)
|| (x1 <= x && x <= x2) ){
y3 = (x - x1) / (x2 - x1) * (y2 - y1) + y1;
if( y3 > y ){
up++;
}
// if( y3 < y ){
// down++;
// }
} else {
continue;
}
}
if( up % 2 === 0 ){
return false;
} else {
return true;
}
};
export const pointInsidePolygon = ( x, y, basePoints, centerX, centerY, width, height, direction, padding ) => {
let transformedPoints = new Array( basePoints.length );
// Gives negative angle
let angle;
if( direction[0] != null ){
angle = Math.atan( direction[1] / direction[0] );
if( direction[0] < 0 ){
angle = angle + Math.PI / 2;
} else {
angle = -angle - Math.PI / 2;
}
} else {
angle = direction;
}
let cos = Math.cos( -angle );
let sin = Math.sin( -angle );
// console.log("base: " + basePoints);
for( let i = 0; i < transformedPoints.length / 2; i++ ){
transformedPoints[ i * 2] =
width / 2 * (basePoints[ i * 2] * cos
- basePoints[ i * 2 + 1] * sin);
transformedPoints[ i * 2 + 1] =
height / 2 * (basePoints[ i * 2 + 1] * cos
+ basePoints[ i * 2] * sin);
transformedPoints[ i * 2] += centerX;
transformedPoints[ i * 2 + 1] += centerY;
}
let points;
if( padding > 0 ){
let expandedLineSet = expandPolygon(
transformedPoints,
-padding );
points = joinLines( expandedLineSet );
} else {
points = transformedPoints;
}
return pointInsidePolygonPoints( x, y, points );
};
export const pointInsideRoundPolygon = (x, y, basePoints, centerX, centerY, width, height, corners) => {
const cutPolygonPoints = new Array( basePoints.length * 2 );
for( let i = 0; i < corners.length; i++ ){
let corner = corners[i];
cutPolygonPoints[i * 4 + 0] = corner.startX;
cutPolygonPoints[i * 4 + 1] = corner.startY;
cutPolygonPoints[i * 4 + 2] = corner.stopX;
cutPolygonPoints[i * 4 + 3] = corner.stopY;
const squaredDistance = Math.pow(corner.cx - x, 2 ) + Math.pow(corner.cy - y, 2 );
if( squaredDistance <= Math.pow( corner.radius, 2 ) ){
return true;
}
}
return pointInsidePolygonPoints(x, y, cutPolygonPoints);
};
export const joinLines = ( lineSet ) => {
let vertices = new Array( lineSet.length / 2 );
let currentLineStartX, currentLineStartY, currentLineEndX, currentLineEndY;
let nextLineStartX, nextLineStartY, nextLineEndX, nextLineEndY;
for( let i = 0; i < lineSet.length / 4; i++ ){
currentLineStartX = lineSet[ i * 4];
currentLineStartY = lineSet[ i * 4 + 1];
currentLineEndX = lineSet[ i * 4 + 2];
currentLineEndY = lineSet[ i * 4 + 3];
if( i < lineSet.length / 4 - 1 ){
nextLineStartX = lineSet[ (i + 1) * 4];
nextLineStartY = lineSet[ (i + 1) * 4 + 1];
nextLineEndX = lineSet[ (i + 1) * 4 + 2];
nextLineEndY = lineSet[ (i + 1) * 4 + 3];
} else {
nextLineStartX = lineSet[0];
nextLineStartY = lineSet[1];
nextLineEndX = lineSet[2];
nextLineEndY = lineSet[3];
}
let intersection = finiteLinesIntersect(
currentLineStartX, currentLineStartY,
currentLineEndX, currentLineEndY,
nextLineStartX, nextLineStartY,
nextLineEndX, nextLineEndY,
true );
vertices[ i * 2] = intersection[0];
vertices[ i * 2 + 1] = intersection[1];
}
return vertices;
};
export const expandPolygon = ( points, pad ) => {
let expandedLineSet = new Array( points.length * 2 );
let currentPointX, currentPointY, nextPointX, nextPointY;
for( let i = 0; i < points.length / 2; i++ ){
currentPointX = points[ i * 2];
currentPointY = points[ i * 2 + 1];
if( i < points.length / 2 - 1 ){
nextPointX = points[ (i + 1) * 2];
nextPointY = points[ (i + 1) * 2 + 1];
} else {
nextPointX = points[0];
nextPointY = points[1];
}
// Current line: [currentPointX, currentPointY] to [nextPointX, nextPointY]
// Assume CCW polygon winding
let offsetX = (nextPointY - currentPointY);
let offsetY = -(nextPointX - currentPointX);
// Normalize
let offsetLength = Math.sqrt( offsetX * offsetX + offsetY * offsetY );
let normalizedOffsetX = offsetX / offsetLength;
let normalizedOffsetY = offsetY / offsetLength;
expandedLineSet[ i * 4] = currentPointX + normalizedOffsetX * pad;
expandedLineSet[ i * 4 + 1] = currentPointY + normalizedOffsetY * pad;
expandedLineSet[ i * 4 + 2] = nextPointX + normalizedOffsetX * pad;
expandedLineSet[ i * 4 + 3] = nextPointY + normalizedOffsetY * pad;
}
return expandedLineSet;
};
export const intersectLineEllipse = ( x, y, centerX, centerY, ellipseWradius, ellipseHradius ) => {
let dispX = centerX - x;
let dispY = centerY - y;
dispX /= ellipseWradius;
dispY /= ellipseHradius;
let len = Math.sqrt( dispX * dispX + dispY * dispY );
let newLength = len - 1;
if( newLength < 0 ){
return [];
}
let lenProportion = newLength / len;
return [ (centerX - x) * lenProportion + x, (centerY - y) * lenProportion + y ];
};
export const checkInEllipse = ( x, y, width, height, centerX, centerY, padding ) => {
x -= centerX;
y -= centerY;
x /= (width / 2 + padding);
y /= (height / 2 + padding);
return x * x + y * y <= 1;
};
// Returns intersections of increasing distance from line's start point
export const intersectLineCircle = ( x1, y1, x2, y2, centerX, centerY, radius ) => {
// Calculate d, direction vector of line
let d = [ x2 - x1, y2 - y1 ]; // Direction vector of line
let f = [ x1 - centerX, y1 - centerY ];
let a = d[0] * d[0] + d[1] * d[1];
let b = 2 * (f[0] * d[0] + f[1] * d[1]);
let c = (f[0] * f[0] + f[1] * f[1]) - radius * radius ;
let discriminant = b * b - 4 * a * c;
if( discriminant < 0 ){
return [];
}
let t1 = (-b + Math.sqrt( discriminant )) / (2 * a);
let t2 = (-b - Math.sqrt( discriminant )) / (2 * a);
let tMin = Math.min( t1, t2 );
let tMax = Math.max( t1, t2 );
let inRangeParams = [];
if( tMin >= 0 && tMin <= 1 ){
inRangeParams.push( tMin );
}
if( tMax >= 0 && tMax <= 1 ){
inRangeParams.push( tMax );
}
if( inRangeParams.length === 0 ){
return [];
}
let nearIntersectionX = inRangeParams[0] * d[0] + x1;
let nearIntersectionY = inRangeParams[0] * d[1] + y1;
if( inRangeParams.length > 1 ){
if( inRangeParams[0] == inRangeParams[1] ){
return [ nearIntersectionX, nearIntersectionY ];
} else {
let farIntersectionX = inRangeParams[1] * d[0] + x1;
let farIntersectionY = inRangeParams[1] * d[1] + y1;
return [ nearIntersectionX, nearIntersectionY, farIntersectionX, farIntersectionY ];
}
} else {
return [ nearIntersectionX, nearIntersectionY ];
}
};
export const findCircleNearPoint = ( centerX, centerY, radius, farX, farY ) => {
let displacementX = farX - centerX;
let displacementY = farY - centerY;
let distance = Math.sqrt( displacementX * displacementX
+ displacementY * displacementY );
let unitDisplacementX = displacementX / distance;
let unitDisplacementY = displacementY / distance;
return [ centerX + unitDisplacementX * radius,
centerY + unitDisplacementY * radius ];
};
export const findMaxSqDistanceToOrigin = ( points ) => {
let maxSqDistance = 0.000001;
let sqDistance;
for( let i = 0; i < points.length / 2; i++ ){
sqDistance = points[ i * 2] * points[ i * 2]
+ points[ i * 2 + 1] * points[ i * 2 + 1];
if( sqDistance > maxSqDistance ){
maxSqDistance = sqDistance;
}
}
return maxSqDistance;
};
export const midOfThree = ( a, b, c ) => {
if( (b <= a && a <= c) || (c <= a && a <= b) ){
return a;
} else if( (a <= b && b <= c) || (c <= b && b <= a) ){
return b;
} else {
return c;
}
};
// (x1,y1)=>(x2,y2) intersect with (x3,y3)=>(x4,y4)
export const finiteLinesIntersect = (
x1, y1, x2, y2,
x3, y3, x4, y4,
infiniteLines
) => {
let dx13 = x1 - x3;
let dx21 = x2 - x1;
let dx43 = x4 - x3;
let dy13 = y1 - y3;
let dy21 = y2 - y1;
let dy43 = y4 - y3;
let ua_t = dx43 * dy13 - dy43 * dx13;
let ub_t = dx21 * dy13 - dy21 * dx13;
let u_b = dy43 * dx21 - dx43 * dy21;
if( u_b !== 0 ){
let ua = ua_t / u_b;
let ub = ub_t / u_b;
let flptThreshold = 0.001;
let min = 0 - flptThreshold;
let max = 1 + flptThreshold;
if( min <= ua && ua <= max && min <= ub && ub <= max ){
return [ x1 + ua * dx21, y1 + ua * dy21 ];
} else {
if( !infiniteLines ){
return [];
} else {
return [ x1 + ua * dx21, y1 + ua * dy21 ];
}
}
} else {
if( ua_t === 0 || ub_t === 0 ){
// Parallel, coincident lines. Check if overlap
// Check endpoint of second line
if( midOfThree( x1, x2, x4 ) === x4 ){
return [ x4, y4 ];
}
// Check start point of second line
if( midOfThree( x1, x2, x3 ) === x3 ){
return [ x3, y3 ];
}
// Endpoint of first line
if( midOfThree( x3, x4, x2 ) === x2 ){
return [ x2, y2 ];
}
return [];
} else {
// Parallel, non-coincident
return [];
}
}
};
export const transformPoints = ( points, centerX, centerY, width, height ) => {
let ret = [];
var halfW = width / 2;
var halfH = height / 2;
let x = centerX;
let y = centerY;
ret.push({
x: x + halfW * points[0],
y: y + halfH * points[1]
});
for( var i = 1; i < points.length / 2; i++ ){
ret.push({
x: x + halfW * points[i * 2],
y: y + halfH * points[i * 2 + 1]
});
}
return ret;
};
// math.polygonIntersectLine( x, y, basePoints, centerX, centerY, width, height, padding )
// intersect a node polygon (pts transformed)
//
// math.polygonIntersectLine( x, y, basePoints, centerX, centerY )
// intersect the points (no transform)
export const polygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding ) => {
let intersections = [];
let intersection;
let transformedPoints = new Array( basePoints.length );
let doTransform = true;
if( width == null ){
doTransform = false;
}
let points;
if( doTransform ){
for( let i = 0; i < transformedPoints.length / 2; i++ ){
transformedPoints[ i * 2] = basePoints[ i * 2] * width + centerX;
transformedPoints[ i * 2 + 1] = basePoints[ i * 2 + 1] * height + centerY;
}
if( padding > 0 ){
let expandedLineSet = expandPolygon(
transformedPoints,
-padding );
points = joinLines( expandedLineSet );
} else {
points = transformedPoints;
}
} else {
points = basePoints;
}
let currentX, currentY, nextX, nextY;
for( let i = 0; i < points.length / 2; i++ ){
currentX = points[ i * 2];
currentY = points[ i * 2 + 1];
if( i < points.length / 2 - 1 ){
nextX = points[ (i + 1) * 2];
nextY = points[ (i + 1) * 2 + 1];
} else {
nextX = points[0];
nextY = points[1];
}
intersection = finiteLinesIntersect(
x, y, centerX, centerY,
currentX, currentY,
nextX, nextY );
if( intersection.length !== 0 ){
intersections.push( intersection[0], intersection[1] );
}
}
return intersections;
};
export const roundPolygonIntersectLine = ( x, y, basePoints, centerX, centerY, width, height, padding, corners ) => {
let intersections = [];
let intersection;
let lines = new Array(basePoints.length * 2);
corners.forEach( (corner, i) => {
if (i === 0) {
lines[lines.length - 2] = corner.startX;
lines[lines.length - 1] = corner.startY;
} else {
lines[i * 4 - 2] = corner.startX;
lines[i * 4 - 1] = corner.startY;
}
lines[i * 4] = corner.stopX;
lines[i * 4 + 1] = corner.stopY;
intersection = intersectLineCircle(x, y, centerX, centerY, corner.cx, corner.cy, corner.radius);
if (intersection.length !== 0) {
intersections.push(intersection[0], intersection[1]);
}
});
for( let i = 0; i < lines.length / 4; i++ ) {
intersection = finiteLinesIntersect(
x, y, centerX, centerY,
lines[i * 4], lines[i * 4 + 1],
lines[i * 4 + 2], lines[i * 4 + 3], false );
if( intersection.length !== 0 ){
intersections.push( intersection[0], intersection[1] );
}
}
if (intersections.length > 2) {
let lowestIntersection = [ intersections[0], intersections[1] ];
let lowestSquaredDistance = Math.pow(lowestIntersection[0] - x, 2) + Math.pow(lowestIntersection[1] - y, 2);
for ( let i = 1; i < intersections.length / 2; i++){
const squaredDistance = Math.pow(intersections[ i * 2 ] - x, 2) + Math.pow(intersections[ i * 2 + 1 ] - y, 2);
if ( squaredDistance <= lowestSquaredDistance ){
lowestIntersection[0] = intersections[ i * 2 ];
lowestIntersection[1] = intersections[ i * 2 + 1 ];
lowestSquaredDistance = squaredDistance;
}
}
return lowestIntersection;
}
return intersections;
};
export const shortenIntersection = ( intersection, offset, amount ) => {
let disp = [ intersection[0] - offset[0], intersection[1] - offset[1] ];
let length = Math.sqrt( disp[0] * disp[0] + disp[1] * disp[1] );
let lenRatio = (length - amount) / length;
if( lenRatio < 0 ){
lenRatio = 0.00001;
}
return [ offset[0] + lenRatio * disp[0], offset[1] + lenRatio * disp[1] ];
};
export const generateUnitNgonPointsFitToSquare = ( sides, rotationRadians ) => {
let points = generateUnitNgonPoints( sides, rotationRadians );
points = fitPolygonToSquare( points );
return points;
};
export const fitPolygonToSquare = ( points ) => {
let x, y;
let sides = points.length / 2;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for( let i = 0; i < sides; i++ ){
x = points[2 * i ];
y = points[2 * i + 1];
minX = Math.min( minX, x );
maxX = Math.max( maxX, x );
minY = Math.min( minY, y );
maxY = Math.max( maxY, y );
}
// stretch factors
let sx = 2 / (maxX - minX);
let sy = 2 / (maxY - minY);
for( let i = 0; i < sides; i++ ){
x = points[2 * i ] = points[2 * i ] * sx;
y = points[2 * i + 1] = points[2 * i + 1] * sy;
minX = Math.min( minX, x );
maxX = Math.max( maxX, x );
minY = Math.min( minY, y );
maxY = Math.max( maxY, y );
}
if( minY < -1 ){
for( let i = 0; i < sides; i++ ){
y = points[2 * i + 1] = points[2 * i + 1] + (-1 - minY);
}
}
return points;
};
export const generateUnitNgonPoints = ( sides, rotationRadians ) => {
let increment = 1.0 / sides * 2 * Math.PI;
let startAngle = sides % 2 === 0 ?
Math.PI / 2.0 + increment / 2.0 : Math.PI / 2.0;
startAngle += rotationRadians;
let points = new Array( sides * 2 );
let currentAngle;
for( let i = 0; i < sides; i++ ){
currentAngle = i * increment + startAngle;
points[2 * i ] = Math.cos( currentAngle ); // x
points[2 * i + 1] = Math.sin( -currentAngle ); // y
}
return points;
};
// Set the default radius, unless half of width or height is smaller than default
export const getRoundRectangleRadius = ( width, height ) =>
Math.min( width / 4, height / 4, 8 );
// Set the default radius
export const getRoundPolygonRadius = (width, height ) =>
Math.min( width / 10, height / 10, 8 );
export const getCutRectangleCornerLength = () => 8;
export const bezierPtsToQuadCoeff = ( p0, p1, p2 ) => [
p0 - 2 * p1 + p2,
2 * ( p1 - p0 ),
p0
];
// get curve width, height, and control point position offsets as a percentage of node height / width
export const getBarrelCurveConstants = ( width, height ) => ({
heightOffset: Math.min(15, 0.05 * height),
widthOffset: Math.min(100, 0.25 * width),
ctrlPtOffsetPct: 0.05
});
// Separating Axis Theorem (SAT) to determine if two polygons intersect.
// The function takes two polygons as input and returns a boolean value indicating
// whether the two polygons intersect.
export function satPolygonIntersection(poly1, poly2) {
function getAxes(polygon) {
let axes = [];
for (let i = 0; i < polygon.length; i++) {
let p1 = polygon[i];
let p2 = polygon[(i + 1) % polygon.length];
let edge = { x: p2.x - p1.x, y: p2.y - p1.y };
let normal = { x: -edge.y, y: edge.x };
let length = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
axes.push({ x: normal.x / length, y: normal.y / length });
}
return axes;
}
function project(polygon, axis) {
let min = Infinity;
let max = -Infinity;
for (let point of polygon) {
let projection = point.x * axis.x + point.y * axis.y;
min = Math.min(min, projection);
max = Math.max(max, projection);
}
return { min, max };
}
function overlaps(proj1, proj2) {
return !(proj1.max < proj2.min || proj2.max < proj1.min);
}
let axes = [...getAxes(poly1), ...getAxes(poly2)];
for (let axis of axes) {
let proj1 = project(poly1, axis);
let proj2 = project(poly2, axis);
if (!overlaps(proj1, proj2)) {
return false; // No overlap, so the polygons do not intersect
}
}
return true; // polygons intersect
}