mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-26 23:43:40 +00:00
Address PR review comments
Edge calculation improvements: - Add zero radius/radii guards in circle and ellipse intersection functions - Add clamping for pill straight edge intersections to prevent overflow - Ensure intersection points stay within valid pill boundaries Handle improvements: - Add bidirectional connection support with overlapping source/target handles - Each edge now has both source and target handles (8 total per node) - Allows edges to connect in any direction from any side - Fixes handle type restrictions that prevented flexible connections Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
318cdee15c
commit
4b865762a1
3 changed files with 180 additions and 10 deletions
|
|
@ -82,10 +82,13 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
|||
}}
|
||||
>
|
||||
{/* Invisible handles positioned around edges - center remains free for dragging */}
|
||||
{/* Top edge handle */}
|
||||
{/* Bidirectional handles (source + target overlapping at each edge) */}
|
||||
|
||||
{/* Top edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="top-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
|
|
@ -99,10 +102,29 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
|||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
{/* Right edge handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
id="top-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "30px",
|
||||
top: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
transform: "none",
|
||||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Right edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
id="right-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "30px",
|
||||
|
|
@ -116,10 +138,29 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
|||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
{/* Bottom edge handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="right-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
right: 0,
|
||||
opacity: 0,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
transform: "none",
|
||||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Bottom}
|
||||
id="bottom-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
|
|
@ -133,10 +174,46 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
|||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
{/* Left edge handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "30px",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
transform: "none",
|
||||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Left edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="left-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
transform: "none",
|
||||
cursor: "crosshair",
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id="left-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: "30px",
|
||||
|
|
|
|||
|
|
@ -221,10 +221,13 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
|||
}}
|
||||
>
|
||||
{/* Invisible handles positioned around edges - center remains free for dragging */}
|
||||
{/* Top edge handle */}
|
||||
{/* Bidirectional handles (source + target overlapping at each edge) */}
|
||||
|
||||
{/* Top edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="top-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '100%',
|
||||
|
|
@ -238,10 +241,29 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
|||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
{/* Right edge handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
id="top-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
top: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
transform: 'none',
|
||||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Right edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
id="right-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '30px',
|
||||
|
|
@ -255,10 +277,29 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
|||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
{/* Bottom edge handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="right-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '30px',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
right: 0,
|
||||
opacity: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
transform: 'none',
|
||||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Bottom edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Bottom}
|
||||
id="bottom-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '100%',
|
||||
|
|
@ -272,10 +313,46 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
|||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
{/* Left edge handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
transform: 'none',
|
||||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Left edge handles */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id="left-target"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '30px',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
opacity: 0,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
transform: 'none',
|
||||
cursor: 'crosshair',
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id="left-source"
|
||||
isConnectable={true}
|
||||
style={{
|
||||
width: '30px',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ function getCircleIntersection(
|
|||
targetY: number,
|
||||
offset: number = 3
|
||||
): { x: number; y: number; angle: number } {
|
||||
// Guard against zero radius
|
||||
if (radius === 0) {
|
||||
return { x: centerX + offset, y: centerY, angle: 0 };
|
||||
}
|
||||
|
||||
const dx = targetX - centerX;
|
||||
const dy = targetY - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
|
@ -53,6 +58,11 @@ function getEllipseIntersection(
|
|||
targetY: number,
|
||||
offset: number = 3
|
||||
): { x: number; y: number; angle: number } {
|
||||
// Guard against zero radii
|
||||
if (radiusX === 0 || radiusY === 0) {
|
||||
return { x: centerX + offset, y: centerY, angle: 0 };
|
||||
}
|
||||
|
||||
const dx = targetX - centerX;
|
||||
const dy = targetY - centerY;
|
||||
|
||||
|
|
@ -131,10 +141,13 @@ function getPillIntersection(
|
|||
// Calculate x position where line from target to center intersects the horizontal edge
|
||||
// Line equation: (y - centerY) / (x - centerX) = dy / dx
|
||||
// Solving for x when y = intersectY: x = centerX + dx * (intersectY - centerY) / dy
|
||||
const intersectX = Math.abs(dy) > 0.001
|
||||
let intersectX = Math.abs(dy) > 0.001
|
||||
? centerX + dx * (intersectY - centerY) / dy
|
||||
: centerX;
|
||||
|
||||
// Clamp intersection to the straight horizontal segment between the caps
|
||||
intersectX = Math.min(Math.max(intersectX, leftCapX), rightCapX);
|
||||
|
||||
const normalAngle = side < 0 ? -Math.PI / 2 : Math.PI / 2;
|
||||
|
||||
return {
|
||||
|
|
@ -164,10 +177,13 @@ function getPillIntersection(
|
|||
// Calculate y position where line from target to center intersects the vertical edge
|
||||
// Line equation: (y - centerY) / (x - centerX) = dy / dx
|
||||
// Solving for y when x = intersectX: y = centerY + dy * (intersectX - centerX) / dx
|
||||
const intersectY = Math.abs(dx) > 0.001
|
||||
let intersectY = Math.abs(dx) > 0.001
|
||||
? centerY + dy * (intersectX - centerX) / dx
|
||||
: centerY;
|
||||
|
||||
// Clamp intersection to the straight vertical segment between the caps
|
||||
intersectY = Math.min(Math.max(intersectY, topCapY), bottomCapY);
|
||||
|
||||
const normalAngle = side < 0 ? Math.PI : 0;
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue