@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
525 lines (482 loc) • 14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.getDirectionFromSide = getDirectionFromSide;
exports.getNormalFromSide = getNormalFromSide;
exports.intersectEllipsePerimeter = intersectEllipsePerimeter;
exports.intersectRectPerimeter = intersectRectPerimeter;
exports.intersectRoundedRectPerimeter = intersectRoundedRectPerimeter;
exports.isPointInEllipse = isPointInEllipse;
exports.isPointInRect = isPointInRect;
var _edgeTypes = require("./edgeTypes.js");
/**
* Edge & Arrow System - Perimeter Intersection
* Module B1: Perimeter intersection for rect, rounded-rect, ellipse
*/
// ============================================================================
// SHAPE TYPES
// ============================================================================
/**
* @typedef {Object} Rect
* @property {number} x - Left edge
* @property {number} y - Top edge
* @property {number} width - Width
* @property {number} height - Height
*/
/**
* @typedef {Object} RoundedRect
* @property {number} x - Left edge
* @property {number} y - Top edge
* @property {number} width - Width
* @property {number} height - Height
* @property {number} rx - X radius of corners
* @property {number} ry - Y radius of corners
*/
/**
* @typedef {Object} Ellipse
* @property {number} cx - Center X
* @property {number} cy - Center Y
* @property {number} rx - X radius
* @property {number} ry - Y radius
*/
/**
* @typedef {Object} IntersectionResult
* @property {Point} point - Intersection point
* @property {Side} side - Side where intersection occurred
*/
// ============================================================================
// RECTANGLE PERIMETER INTERSECTION
// ============================================================================
/**
* Find intersection of a ray with rectangle perimeter
* @param {Rect} rect - Rectangle bounds
* @param {Point} origin - Ray origin point
* @param {Point} direction - Ray direction vector (normalized)
* @returns {IntersectionResult} Intersection point and side
*/
function intersectRectPerimeter(rect, origin, direction) {
if (!rect || !origin || !direction) {
return null;
}
const {
x,
y,
width,
height
} = rect;
const right = x + width;
const bottom = y + height;
// Calculate intersection with each edge
const intersections = [];
// Top edge (y = rect.y)
if (Math.abs(direction.y) > 1e-6) {
const t = (y - origin.y) / direction.y;
if (t > 0) {
const ix = origin.x + direction.x * t;
if (ix >= x && ix <= right) {
intersections.push({
t,
point: {
x: ix,
y
},
side: _edgeTypes.SIDES.NORTH
});
}
}
}
// Right edge (x = rect.x + width)
if (Math.abs(direction.x) > 1e-6) {
const t = (right - origin.x) / direction.x;
if (t > 0) {
const iy = origin.y + direction.y * t;
if (iy >= y && iy <= bottom) {
intersections.push({
t,
point: {
x: right,
y: iy
},
side: _edgeTypes.SIDES.EAST
});
}
}
}
// Bottom edge (y = rect.y + height)
if (Math.abs(direction.y) > 1e-6) {
const t = (bottom - origin.y) / direction.y;
if (t > 0) {
const ix = origin.x + direction.x * t;
if (ix >= x && ix <= right) {
intersections.push({
t,
point: {
x: ix,
y: bottom
},
side: _edgeTypes.SIDES.SOUTH
});
}
}
}
// Left edge (x = rect.x)
if (Math.abs(direction.x) > 1e-6) {
const t = (x - origin.x) / direction.x;
if (t > 0) {
const iy = origin.y + direction.y * t;
if (iy >= y && iy <= bottom) {
intersections.push({
t,
point: {
x,
y: iy
},
side: _edgeTypes.SIDES.WEST
});
}
}
}
// Return closest intersection
if (intersections.length === 0) {
return null;
}
const closest = intersections.reduce((min, current) => current.t < min.t ? current : min);
return {
point: closest.point,
side: closest.side
};
}
// ============================================================================
// ROUNDED RECTANGLE PERIMETER INTERSECTION
// ============================================================================
/**
* Find intersection of a ray with rounded rectangle perimeter
* @param {RoundedRect} roundedRect - Rounded rectangle bounds
* @param {Point} origin - Ray origin point
* @param {Point} direction - Ray direction vector (normalized)
* @returns {IntersectionResult} Intersection point and side
*/
function intersectRoundedRectPerimeter(roundedRect, origin, direction) {
if (!roundedRect || !origin || !direction) {
return null;
}
const {
x,
y,
width,
height,
rx,
ry
} = roundedRect;
const right = x + width;
const bottom = y + height;
// Clamp corner radii to prevent overlap
const maxRx = Math.min(rx, width / 2);
const maxRy = Math.min(ry, height / 2);
// Check if ray intersects with corner circles
const cornerIntersections = [];
// Top-left corner
const tlCenter = {
x: x + maxRx,
y: y + maxRy
};
const tlIntersection = intersectCirclePerimeter(tlCenter, maxRx, maxRy, origin, direction);
if (tlIntersection && tlIntersection.point.x <= x + maxRx && tlIntersection.point.y <= y + maxRy) {
cornerIntersections.push({
...tlIntersection,
side: _edgeTypes.SIDES.NORTH
});
}
// Top-right corner
const trCenter = {
x: right - maxRx,
y: y + maxRy
};
const trIntersection = intersectCirclePerimeter(trCenter, maxRx, maxRy, origin, direction);
if (trIntersection && trIntersection.point.x >= right - maxRx && trIntersection.point.y <= y + maxRy) {
cornerIntersections.push({
...trIntersection,
side: _edgeTypes.SIDES.NORTH
});
}
// Bottom-right corner
const brCenter = {
x: right - maxRx,
y: bottom - maxRy
};
const brIntersection = intersectCirclePerimeter(brCenter, maxRx, maxRy, origin, direction);
if (brIntersection && brIntersection.point.x >= right - maxRx && brIntersection.point.y >= bottom - maxRy) {
cornerIntersections.push({
...brIntersection,
side: _edgeTypes.SIDES.SOUTH
});
}
// Bottom-left corner
const blCenter = {
x: x + maxRx,
y: bottom - maxRy
};
const blIntersection = intersectCirclePerimeter(blCenter, maxRx, maxRy, origin, direction);
if (blIntersection && blIntersection.point.x <= x + maxRx && blIntersection.point.y >= bottom - maxRy) {
cornerIntersections.push({
...blIntersection,
side: _edgeTypes.SIDES.SOUTH
});
}
// Check straight edges (excluding corner areas)
const straightIntersections = [];
// Top edge (excluding corners)
if (Math.abs(direction.y) > 1e-6) {
const t = (y - origin.y) / direction.y;
if (t > 0) {
const ix = origin.x + direction.x * t;
if (ix >= x + maxRx && ix <= right - maxRx) {
straightIntersections.push({
t,
point: {
x: ix,
y
},
side: _edgeTypes.SIDES.NORTH
});
}
}
}
// Right edge (excluding corners)
if (Math.abs(direction.x) > 1e-6) {
const t = (right - origin.x) / direction.x;
if (t > 0) {
const iy = origin.y + direction.y * t;
if (iy >= y + maxRy && iy <= bottom - maxRy) {
straightIntersections.push({
t,
point: {
x: right,
y: iy
},
side: _edgeTypes.SIDES.EAST
});
}
}
}
// Bottom edge (excluding corners)
if (Math.abs(direction.y) > 1e-6) {
const t = (bottom - origin.y) / direction.y;
if (t > 0) {
const ix = origin.x + direction.x * t;
if (ix >= x + maxRx && ix <= right - maxRx) {
straightIntersections.push({
t,
point: {
x: ix,
y: bottom
},
side: _edgeTypes.SIDES.SOUTH
});
}
}
}
// Left edge (excluding corners)
if (Math.abs(direction.x) > 1e-6) {
const t = (x - origin.x) / direction.x;
if (t > 0) {
const iy = origin.y + direction.y * t;
if (iy >= y + maxRy && iy <= bottom - maxRy) {
straightIntersections.push({
t,
point: {
x,
y: iy
},
side: _edgeTypes.SIDES.WEST
});
}
}
}
// Combine all intersections and find closest
const allIntersections = [...cornerIntersections, ...straightIntersections];
if (allIntersections.length === 0) {
return null;
}
const closest = allIntersections.reduce((min, current) => current.t < min.t ? current : min);
return {
point: closest.point,
side: closest.side
};
}
// ============================================================================
// ELLIPSE PERIMETER INTERSECTION
// ============================================================================
/**
* Find intersection of a ray with ellipse perimeter
* @param {Ellipse} ellipse - Ellipse parameters
* @param {Point} origin - Ray origin point
* @param {Point} direction - Ray direction vector (normalized)
* @returns {IntersectionResult} Intersection point and side
*/
function intersectEllipsePerimeter(ellipse, origin, direction) {
if (!ellipse || !origin || !direction) {
return null;
}
const {
cx,
cy,
rx,
ry
} = ellipse;
// Transform to unit circle space
const dx = origin.x - cx;
const dy = origin.y - cy;
const ux = direction.x / rx;
const uy = direction.y / ry;
// Solve quadratic equation for intersection
const a = ux * ux + uy * uy;
const b = 2 * (dx * ux / rx + dy * uy / ry);
const c = dx / rx * (dx / rx) + dy / ry * (dy / ry) - 1;
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return null; // No intersection
}
const sqrtDisc = Math.sqrt(discriminant);
const t1 = (-b - sqrtDisc) / (2 * a);
const t2 = (-b + sqrtDisc) / (2 * a);
// Use positive t value (ray going outward)
const t = t1 > 0 ? t1 : t2 > 0 ? t2 : null;
if (t === null || t <= 0) {
return null;
}
// Calculate intersection point
const point = {
x: origin.x + direction.x * t,
y: origin.y + direction.y * t
};
// Determine side based on angle from center
const angle = Math.atan2(point.y - cy, point.x - cx);
const side = getSideFromAngle(angle);
return {
point,
side
};
}
/**
* Find intersection of a ray with circle perimeter (for rounded rect corners)
* @param {Point} center - Circle center
* @param {number} rx - X radius
* @param {number} ry - Y radius
* @param {Point} origin - Ray origin point
* @param {Point} direction - Ray direction vector (normalized)
* @returns {IntersectionResult} Intersection point and side
*/
function intersectCirclePerimeter(center, rx, ry, origin, direction) {
// Treat as ellipse with given radii
const ellipse = {
cx: center.x,
cy: center.y,
rx,
ry
};
return intersectEllipsePerimeter(ellipse, origin, direction);
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Get side from angle (in radians)
* @param {number} angle - Angle in radians
* @returns {Side} Side constant
*/
function getSideFromAngle(angle) {
// Normalize angle to [0, 2π)
const normalizedAngle = (angle % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
// Map angle ranges to sides
if (normalizedAngle >= Math.PI / 4 && normalizedAngle < 3 * Math.PI / 4) {
return _edgeTypes.SIDES.SOUTH; // 45° to 135°
} else if (normalizedAngle >= 3 * Math.PI / 4 && normalizedAngle < 5 * Math.PI / 4) {
return _edgeTypes.SIDES.WEST; // 135° to 225°
} else if (normalizedAngle >= 5 * Math.PI / 4 && normalizedAngle < 7 * Math.PI / 4) {
return _edgeTypes.SIDES.NORTH; // 225° to 315°
} else {
return _edgeTypes.SIDES.EAST; // 315° to 45°
}
}
/**
* Create a direction vector from side
* @param {Side} side - Side constant
* @returns {Point} Normalized direction vector
*/
function getDirectionFromSide(side) {
switch (side) {
case _edgeTypes.SIDES.NORTH:
return {
x: 0,
y: -1
};
case _edgeTypes.SIDES.EAST:
return {
x: 1,
y: 0
};
case _edgeTypes.SIDES.SOUTH:
return {
x: 0,
y: 1
};
case _edgeTypes.SIDES.WEST:
return {
x: -1,
y: 0
};
default:
return {
x: 0,
y: 0
};
}
}
/**
* Get the normal vector for a side (outward pointing)
* @param {Side} side - Side constant
* @returns {Point} Normal vector
*/
function getNormalFromSide(side) {
return getDirectionFromSide(side);
}
/**
* Check if a point is inside a rectangle
* @param {Point} point - Point to test
* @param {Rect} rect - Rectangle bounds
* @returns {boolean} True if point is inside
*/
function isPointInRect(point, rect) {
if (!point || !rect) return false;
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
}
/**
* Check if a point is inside an ellipse
* @param {Point} point - Point to test
* @param {Ellipse} ellipse - Ellipse parameters
* @returns {boolean} True if point is inside
*/
function isPointInEllipse(point, ellipse) {
if (!point || !ellipse) return false;
const {
cx,
cy,
rx,
ry
} = ellipse;
const dx = point.x - cx;
const dy = point.y - cy;
return dx / rx * (dx / rx) + dy / ry * (dy / ry) <= 1;
}
var _default = exports.default = {
intersectRectPerimeter,
intersectRoundedRectPerimeter,
intersectEllipsePerimeter,
getDirectionFromSide,
getNormalFromSide,
isPointInRect,
isPointInEllipse
};