@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
364 lines (334 loc) • 10.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.anchorPoint = anchorPoint;
exports.createDefaultPortForNode = createDefaultPortForNode;
exports.default = void 0;
exports.getBestSide = getBestSide;
exports.validatePortForNode = validatePortForNode;
var _edgeTypes = require("./edgeTypes.js");
var _perimeterIntersection = require("./perimeterIntersection.js");
/**
* Edge & Arrow System - Anchor Point Resolution
* Module B2: anchorPoint(node, port) resolves side/fixed ports and returns {point, side}
*/
// ============================================================================
// NODE TYPES
// ============================================================================
/**
* @typedef {Object} Node
* @property {string} id - Node identifier
* @property {Point} position - Node position
* @property {number} width - Node width
* @property {number} height - Node height
* @property {string} [shape] - Shape type: 'rect', 'roundedRect', 'ellipse'
* @property {number} [rx] - Corner radius for rounded rectangles
* @property {number} [ry] - Corner radius for rounded rectangles
*/
// ============================================================================
// ANCHOR POINT RESOLUTION
// ============================================================================
/**
* Resolve anchor point for a node and port configuration
* @param {Node} node - Node to anchor to
* @param {Port} port - Port configuration
* @param {Point} [targetPoint] - Optional target point for direction calculation
* @returns {IntersectionResult} {point, side} or null if invalid
*/
function anchorPoint(node, port, targetPoint = null) {
if (!node) {
return null;
}
// Handle fixed coordinates
if (port && typeof port.x === 'number' && typeof port.y === 'number') {
return resolveFixedPort(node, port);
}
// Handle side-based ports
if (port && port.side && (0, _edgeTypes.isValidPort)(port)) {
return resolveSidePort(node, port, targetPoint);
}
// Fallback: auto-determine best side based on target
if (targetPoint) {
return resolveAutoPort(node, targetPoint);
}
return null;
}
/**
* Resolve fixed coordinate port
* @param {Node} node - Node to anchor to
* @param {Port} port - Port with fixed coordinates
* @returns {IntersectionResult} {point, side}
*/
function resolveFixedPort(node, port) {
const {
x,
y
} = port;
const nodeCenter = {
x: node.position.x + node.width / 2,
y: node.position.y + node.height / 2
};
// Calculate direction from center to fixed point
const direction = normalizeVector({
x: x - nodeCenter.x,
y: y - nodeCenter.y
});
// Find intersection with perimeter
const intersection = findPerimeterIntersection(node, nodeCenter, direction);
if (intersection) {
return {
point: {
x,
y
},
// Use the fixed coordinates
side: intersection.side
};
}
// Fallback: use center point
return {
point: nodeCenter,
side: _edgeTypes.SIDES.EAST
};
}
/**
* Resolve side-based port
* @param {Node} node - Node to anchor to
* @param {Port} port - Port with side constraint
* @param {Point} targetPoint - Target point for alignment
* @returns {IntersectionResult} {point, side}
*/
function resolveSidePort(node, port, targetPoint) {
const {
side,
align = 0.5
} = port;
const nodeCenter = {
x: node.position.x + node.width / 2,
y: node.position.y + node.height / 2
};
// Get direction for the specified side
const direction = (0, _perimeterIntersection.getDirectionFromSide)(side);
// Find intersection with perimeter
const intersection = findPerimeterIntersection(node, nodeCenter, direction);
if (!intersection) {
return null;
}
// Apply alignment along the side
const alignedPoint = applySideAlignment(node, intersection.point, side, align);
return {
point: alignedPoint,
side: side
};
}
/**
* Auto-resolve port based on target direction
* @param {Node} node - Node to anchor to
* @param {Point} targetPoint - Target point
* @returns {IntersectionResult} {point, side}
*/
function resolveAutoPort(node, targetPoint) {
const nodeCenter = {
x: node.position.x + node.width / 2,
y: node.position.y + node.height / 2
};
// Calculate direction from node center to target
const direction = normalizeVector({
x: targetPoint.x - nodeCenter.x,
y: targetPoint.y - nodeCenter.y
});
// Find intersection with perimeter
const intersection = findPerimeterIntersection(node, nodeCenter, direction);
if (!intersection) {
return null;
}
return intersection;
}
// ============================================================================
// PERIMETER INTERSECTION HELPERS
// ============================================================================
/**
* Find intersection with node perimeter based on shape type
* @param {Node} node - Node to intersect with
* @param {Point} origin - Ray origin
* @param {Point} direction - Ray direction
* @returns {IntersectionResult} {point, side} or null
*/
function findPerimeterIntersection(node, origin, direction) {
const shape = node.shape || 'rect';
switch (shape) {
case 'rect':
return (0, _perimeterIntersection.intersectRectPerimeter)({
x: node.position.x,
y: node.position.y,
width: node.width,
height: node.height
}, origin, direction);
case 'roundedRect':
return (0, _perimeterIntersection.intersectRoundedRectPerimeter)({
x: node.position.x,
y: node.position.y,
width: node.width,
height: node.height,
rx: node.rx || 0,
ry: node.ry || 0
}, origin, direction);
case 'ellipse':
return (0, _perimeterIntersection.intersectEllipsePerimeter)({
cx: node.position.x + node.width / 2,
cy: node.position.y + node.height / 2,
rx: node.width / 2,
ry: node.height / 2
}, origin, direction);
default:
// Default to rectangle
return (0, _perimeterIntersection.intersectRectPerimeter)({
x: node.position.x,
y: node.position.y,
width: node.width,
height: node.height
}, origin, direction);
}
}
// ============================================================================
// SIDE ALIGNMENT
// ============================================================================
/**
* Apply alignment along a side
* @param {Node} node - Node
* @param {Point} basePoint - Base intersection point
* @param {Side} side - Side to align along
* @param {number} align - Alignment fraction (0..1)
* @returns {Point} Aligned point
*/
function applySideAlignment(node, basePoint, side, align) {
const {
position,
width,
height
} = node;
switch (side) {
case _edgeTypes.SIDES.NORTH:
case _edgeTypes.SIDES.SOUTH:
// Align horizontally along top/bottom edge
const minX = position.x;
const maxX = position.x + width;
const alignedX = minX + (maxX - minX) * align;
return {
x: alignedX,
y: basePoint.y
};
case _edgeTypes.SIDES.EAST:
case _edgeTypes.SIDES.WEST:
// Align vertically along left/right edge
const minY = position.y;
const maxY = position.y + height;
const alignedY = minY + (maxY - minY) * align;
return {
x: basePoint.x,
y: alignedY
};
default:
return basePoint;
}
}
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Normalize a vector to unit length
* @param {Point} vector - Vector to normalize
* @returns {Point} Normalized vector
*/
function normalizeVector(vector) {
const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
if (length < 1e-6) {
return {
x: 0,
y: 0
};
}
return {
x: vector.x / length,
y: vector.y / length
};
}
/**
* Get the best side for connecting to a target point
* @param {Node} node - Source node
* @param {Point} targetPoint - Target point
* @returns {Side} Best side for connection
*/
function getBestSide(node, targetPoint) {
const nodeCenter = {
x: node.position.x + node.width / 2,
y: node.position.y + node.height / 2
};
const dx = targetPoint.x - nodeCenter.x;
const dy = targetPoint.y - nodeCenter.y;
// Determine best side based on direction
if (Math.abs(dx) > Math.abs(dy)) {
return dx > 0 ? _edgeTypes.SIDES.EAST : _edgeTypes.SIDES.WEST;
} else {
return dy > 0 ? _edgeTypes.SIDES.SOUTH : _edgeTypes.SIDES.NORTH;
}
}
/**
* Create a default port configuration for a node
* @param {Node} node - Node to create port for
* @param {Point} [targetPoint] - Optional target point
* @returns {Port} Default port configuration
*/
function createDefaultPortForNode(node, targetPoint = null) {
if (!node) {
return {
side: _edgeTypes.SIDES.EAST,
sticky: true,
align: 0.5
};
}
if (targetPoint) {
const bestSide = getBestSide(node, targetPoint);
return {
side: bestSide,
sticky: true,
align: 0.5
};
}
return {
side: _edgeTypes.SIDES.EAST,
sticky: true,
align: 0.5
};
}
/**
* Validate that a port configuration is valid for a node
* @param {Node} node - Node to validate against
* @param {Port} port - Port configuration to validate
* @returns {boolean} True if valid
*/
function validatePortForNode(node, port) {
if (!node || !port) {
return false;
}
if (!(0, _edgeTypes.isValidPort)(port)) {
return false;
}
// Check fixed coordinates are within node bounds
if (typeof port.x === 'number' && typeof port.y === 'number') {
const {
position,
width,
height
} = node;
return port.x >= position.x && port.x <= position.x + width && port.y >= position.y && port.y <= position.y + height;
}
return true;
}
var _default = exports.default = {
anchorPoint,
getBestSide,
createDefaultPortForNode,
validatePortForNode
};