mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +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 */}
|
{/* 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
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
|
id="top-target"
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -99,10 +102,29 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
||||||
cursor: "crosshair",
|
cursor: "crosshair",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Right edge handle */}
|
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
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}
|
position={Position.Right}
|
||||||
|
id="right-target"
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: "30px",
|
width: "30px",
|
||||||
|
|
@ -116,10 +138,29 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
||||||
cursor: "crosshair",
|
cursor: "crosshair",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Bottom edge handle */}
|
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
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}
|
position={Position.Bottom}
|
||||||
|
id="bottom-target"
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
@ -133,10 +174,46 @@ const CustomNode = ({ data, selected }: NodeProps<Actor>) => {
|
||||||
cursor: "crosshair",
|
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
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
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}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: "30px",
|
width: "30px",
|
||||||
|
|
|
||||||
|
|
@ -221,10 +221,13 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Invisible handles positioned around edges - center remains free for dragging */}
|
{/* 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
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Top}
|
position={Position.Top}
|
||||||
|
id="top-target"
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -238,10 +241,29 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
||||||
cursor: 'crosshair',
|
cursor: 'crosshair',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Right edge handle */}
|
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
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}
|
position={Position.Right}
|
||||||
|
id="right-target"
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: '30px',
|
width: '30px',
|
||||||
|
|
@ -255,10 +277,29 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
||||||
cursor: 'crosshair',
|
cursor: 'crosshair',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Bottom edge handle */}
|
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
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}
|
position={Position.Bottom}
|
||||||
|
id="bottom-target"
|
||||||
isConnectable={true}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -272,10 +313,46 @@ const GroupNode = ({ id, data, selected }: NodeProps<Group>) => {
|
||||||
cursor: 'crosshair',
|
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
|
<Handle
|
||||||
type="target"
|
type="target"
|
||||||
position={Position.Left}
|
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}
|
isConnectable={true}
|
||||||
style={{
|
style={{
|
||||||
width: '30px',
|
width: '30px',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ function getCircleIntersection(
|
||||||
targetY: number,
|
targetY: number,
|
||||||
offset: number = 3
|
offset: number = 3
|
||||||
): { x: number; y: number; angle: number } {
|
): { 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 dx = targetX - centerX;
|
||||||
const dy = targetY - centerY;
|
const dy = targetY - centerY;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
@ -53,6 +58,11 @@ function getEllipseIntersection(
|
||||||
targetY: number,
|
targetY: number,
|
||||||
offset: number = 3
|
offset: number = 3
|
||||||
): { x: number; y: number; angle: number } {
|
): { 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 dx = targetX - centerX;
|
||||||
const dy = targetY - centerY;
|
const dy = targetY - centerY;
|
||||||
|
|
||||||
|
|
@ -131,10 +141,13 @@ function getPillIntersection(
|
||||||
// Calculate x position where line from target to center intersects the horizontal edge
|
// Calculate x position where line from target to center intersects the horizontal edge
|
||||||
// Line equation: (y - centerY) / (x - centerX) = dy / dx
|
// Line equation: (y - centerY) / (x - centerX) = dy / dx
|
||||||
// Solving for x when y = intersectY: x = centerX + dx * (intersectY - centerY) / dy
|
// 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 + dx * (intersectY - centerY) / dy
|
||||||
: centerX;
|
: 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;
|
const normalAngle = side < 0 ? -Math.PI / 2 : Math.PI / 2;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -164,10 +177,13 @@ function getPillIntersection(
|
||||||
// Calculate y position where line from target to center intersects the vertical edge
|
// Calculate y position where line from target to center intersects the vertical edge
|
||||||
// Line equation: (y - centerY) / (x - centerX) = dy / dx
|
// Line equation: (y - centerY) / (x - centerX) = dy / dx
|
||||||
// Solving for y when x = intersectX: y = centerY + dy * (intersectX - centerX) / 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 + dy * (intersectX - centerX) / dx
|
||||||
: centerY;
|
: 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;
|
const normalAngle = side < 0 ? Math.PI : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue