agent-ecosystem/packages/agent-graph/src/layout/launchAnchor.ts
777genius aed08113e6 feat(agent-graph): integrate stable slot layout for improved node positioning and interaction
- Added stable slot layout support in various components, enhancing the layout and interaction of nodes.
- Updated TypeScript configuration to include new paths for the agent-graph package.
- Refactored layout logic in activity lanes and kanban to accommodate stable slot assignments.
- Enhanced GraphView and GraphControls to support sidebar visibility toggling and owner slot drop handling.
- Introduced new types for layout management in GraphDataPort and related files.
- Updated README to include stable slot layout documentation.
2026-04-15 16:18:11 +03:00

138 lines
4.4 KiB
TypeScript

import { NODE } from '../constants/canvas-constants';
import {
ACTIVITY_ANCHOR_LAYOUT,
resolveActivityLaneSide,
} from './activityLane';
import { createStableSlotLaunchAnchorLayout } from './stableSlotGeometry';
export interface WorldBounds {
left: number;
top: number;
right: number;
bottom: number;
}
export interface LaunchAnchorScreenPlacement {
x: number;
y: number;
scale: number;
visible: boolean;
}
export const LAUNCH_ANCHOR_LAYOUT = createStableSlotLaunchAnchorLayout({
radiusLead: NODE.radiusLead,
});
const LAUNCH_ANCHOR_PREFIX = '__launch_anchor__:';
const ACTIVITY_ANCHOR_PREFIX = '__activity_anchor__:';
export function getLaunchAnchorId(leadNodeId: string): string {
return `${LAUNCH_ANCHOR_PREFIX}${leadNodeId}`;
}
export function getActivityAnchorId(nodeId: string): string {
return `${ACTIVITY_ANCHOR_PREFIX}${nodeId}`;
}
export function isLaunchAnchorId(nodeId: string): boolean {
return nodeId.startsWith(LAUNCH_ANCHOR_PREFIX);
}
export function isActivityAnchorId(nodeId: string): boolean {
return nodeId.startsWith(ACTIVITY_ANCHOR_PREFIX);
}
export function getLaunchAnchorTarget(leadX: number, leadY: number): { x: number; y: number } {
return {
x: leadX + LAUNCH_ANCHOR_LAYOUT.anchorCenterOffsetX,
y: leadY + LAUNCH_ANCHOR_LAYOUT.anchorCenterOffsetY,
};
}
export function getLaunchHudScale(zoom: number): number {
return clamp(zoom, LAUNCH_ANCHOR_LAYOUT.minScale, LAUNCH_ANCHOR_LAYOUT.maxScale);
}
export function getLaunchAnchorBounds(anchorX: number, anchorY: number): WorldBounds {
const halfWidth = LAUNCH_ANCHOR_LAYOUT.compactWidth / 2;
const halfHeight = LAUNCH_ANCHOR_LAYOUT.compactHeight / 2;
return {
left: anchorX - halfWidth,
top: anchorY - halfHeight,
right: anchorX + halfWidth,
bottom: anchorY + halfHeight,
};
}
export const getLaunchHudBounds = getLaunchAnchorBounds;
export const HANDOFF_ANCHOR_LAYOUT = ACTIVITY_ANCHOR_LAYOUT;
export const getHandoffAnchorId = getActivityAnchorId;
export const isHandoffAnchorId = isActivityAnchorId;
export function getHandoffAnchorTarget(args: {
nodeX: number;
nodeY: number;
nodeKind: 'lead' | 'member';
leadX?: number | null;
}): { x: number; y: number } {
const { nodeX, nodeY, nodeKind, leadX } = args;
const side = resolveActivityLaneSide({ nodeKind, nodeX, leadX });
if (side === 'left') {
return {
x: nodeX + ACTIVITY_ANCHOR_LAYOUT.leadOffsetX,
y: nodeY + ACTIVITY_ANCHOR_LAYOUT.leadOffsetY,
};
}
return {
x: nodeX + ACTIVITY_ANCHOR_LAYOUT.memberOffsetX,
y: nodeY + ACTIVITY_ANCHOR_LAYOUT.memberOffsetY,
};
}
export function getHandoffAnchorBounds(anchorX: number, anchorY: number): WorldBounds {
const halfWidth = ACTIVITY_ANCHOR_LAYOUT.reservedWidth / 2;
const halfHeight = ACTIVITY_ANCHOR_LAYOUT.reservedHeight / 2;
return {
left: anchorX - halfWidth,
top: anchorY - halfHeight,
right: anchorX + halfWidth,
bottom: anchorY + halfHeight,
};
}
export function getLaunchAnchorScreenPlacement(args: {
anchorX: number;
anchorY: number;
cameraX: number;
cameraY: number;
zoom: number;
viewportWidth: number;
viewportHeight: number;
}): LaunchAnchorScreenPlacement {
const { anchorX, anchorY, cameraX, cameraY, zoom, viewportWidth, viewportHeight } = args;
const scale = getLaunchHudScale(zoom);
const scaledWidth = LAUNCH_ANCHOR_LAYOUT.compactWidth * scale;
const scaledHeight = LAUNCH_ANCHOR_LAYOUT.compactHeight * scale;
const screenX = anchorX * zoom + cameraX;
const screenY = anchorY * zoom + cameraY;
const rawX = screenX - scaledWidth / 2;
const rawY = screenY - scaledHeight / 2;
const maxX = viewportWidth - scaledWidth - LAUNCH_ANCHOR_LAYOUT.viewportPadding;
const maxY = viewportHeight - scaledHeight - LAUNCH_ANCHOR_LAYOUT.viewportPadding;
return {
x: clamp(rawX, LAUNCH_ANCHOR_LAYOUT.viewportPadding, Math.max(LAUNCH_ANCHOR_LAYOUT.viewportPadding, maxX)),
y: clamp(rawY, LAUNCH_ANCHOR_LAYOUT.viewportPadding, Math.max(LAUNCH_ANCHOR_LAYOUT.viewportPadding, maxY)),
scale,
visible:
screenX > -LAUNCH_ANCHOR_LAYOUT.visiblePadding &&
screenX < viewportWidth + LAUNCH_ANCHOR_LAYOUT.visiblePadding &&
screenY > -LAUNCH_ANCHOR_LAYOUT.visiblePadding &&
screenY < viewportHeight + LAUNCH_ANCHOR_LAYOUT.visiblePadding,
};
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}