fix(agent-graph): use directional central exclusion
This commit is contained in:
parent
77d3e9f7d8
commit
d81a45f15b
2 changed files with 395 additions and 52 deletions
|
|
@ -60,6 +60,7 @@ export interface StableSlotLayoutSnapshot {
|
||||||
launchAnchor: { x: number; y: number } | null;
|
launchAnchor: { x: number; y: number } | null;
|
||||||
leadCentralReservedBlock: StableRect;
|
leadCentralReservedBlock: StableRect;
|
||||||
runtimeCentralExclusion: StableRect;
|
runtimeCentralExclusion: StableRect;
|
||||||
|
centralCollisionRects: StableRect[];
|
||||||
memberSlotFrames: SlotFrame[];
|
memberSlotFrames: SlotFrame[];
|
||||||
memberSlotFrameByOwnerId: Map<string, SlotFrame>;
|
memberSlotFrameByOwnerId: Map<string, SlotFrame>;
|
||||||
unassignedTaskRect: StableRect | null;
|
unassignedTaskRect: StableRect | null;
|
||||||
|
|
@ -113,6 +114,7 @@ const SLOT_GEOMETRY = {
|
||||||
|
|
||||||
const PROCESS_RAIL_NODE_GAP = 42;
|
const PROCESS_RAIL_NODE_GAP = 42;
|
||||||
const PROCESS_RAIL_NODE_FOOTPRINT = 28;
|
const PROCESS_RAIL_NODE_FOOTPRINT = 28;
|
||||||
|
const GEOMETRY_EPSILON = 0.001;
|
||||||
|
|
||||||
const SECTOR_VECTORS = STABLE_SLOT_SECTOR_VECTORS;
|
const SECTOR_VECTORS = STABLE_SLOT_SECTOR_VECTORS;
|
||||||
|
|
||||||
|
|
@ -143,27 +145,30 @@ export function buildStableSlotLayoutSnapshot({
|
||||||
|
|
||||||
const ownerFootprints = computeOwnerFootprints(nodes, layout);
|
const ownerFootprints = computeOwnerFootprints(nodes, layout);
|
||||||
const unassignedTaskRect = buildUnassignedTaskRect(nodes, leadCentralReservedBlock);
|
const unassignedTaskRect = buildUnassignedTaskRect(nodes, leadCentralReservedBlock);
|
||||||
|
const centralCollisionRects = buildCentralCollisionRects({
|
||||||
|
leadCoreRect,
|
||||||
|
leadActivityRect,
|
||||||
|
launchHudRect,
|
||||||
|
unassignedTaskRect,
|
||||||
|
});
|
||||||
const runtimeCentralExclusion = padRect(
|
const runtimeCentralExclusion = padRect(
|
||||||
unionRects(
|
unionRects(centralCollisionRects),
|
||||||
unassignedTaskRect
|
|
||||||
? [leadCentralReservedBlock, unassignedTaskRect]
|
|
||||||
: [leadCentralReservedBlock]
|
|
||||||
),
|
|
||||||
SLOT_GEOMETRY.centralPadding
|
SLOT_GEOMETRY.centralPadding
|
||||||
);
|
);
|
||||||
|
|
||||||
const memberSlotFrames = planOwnerSlots(ownerFootprints, runtimeCentralExclusion, layout);
|
const memberSlotFrames = planOwnerSlots(
|
||||||
|
ownerFootprints,
|
||||||
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
|
layout
|
||||||
|
);
|
||||||
const memberSlotFrameByOwnerId = new Map(
|
const memberSlotFrameByOwnerId = new Map(
|
||||||
memberSlotFrames.map((frame) => [frame.ownerId, frame] as const)
|
memberSlotFrames.map((frame) => [frame.ownerId, frame] as const)
|
||||||
);
|
);
|
||||||
const fitBounds = unionRects(
|
const fitBounds = unionRects(
|
||||||
[
|
[
|
||||||
leadCentralReservedBlock,
|
|
||||||
leadActivityRect,
|
|
||||||
launchHudRect,
|
|
||||||
runtimeCentralExclusion,
|
runtimeCentralExclusion,
|
||||||
...memberSlotFrames.map((frame) => frame.bounds),
|
...memberSlotFrames.map((frame) => frame.bounds),
|
||||||
...(unassignedTaskRect ? [unassignedTaskRect] : []),
|
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -180,6 +185,7 @@ export function buildStableSlotLayoutSnapshot({
|
||||||
},
|
},
|
||||||
leadCentralReservedBlock,
|
leadCentralReservedBlock,
|
||||||
runtimeCentralExclusion,
|
runtimeCentralExclusion,
|
||||||
|
centralCollisionRects,
|
||||||
memberSlotFrames,
|
memberSlotFrames,
|
||||||
memberSlotFrameByOwnerId,
|
memberSlotFrameByOwnerId,
|
||||||
unassignedTaskRect,
|
unassignedTaskRect,
|
||||||
|
|
@ -187,6 +193,33 @@ export function buildStableSlotLayoutSnapshot({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCentralCollisionRects(args: {
|
||||||
|
leadCoreRect: StableRect;
|
||||||
|
leadActivityRect: StableRect;
|
||||||
|
launchHudRect: StableRect;
|
||||||
|
unassignedTaskRect: StableRect | null;
|
||||||
|
}): StableRect[] {
|
||||||
|
const rects = [args.leadCoreRect, args.leadActivityRect, args.launchHudRect];
|
||||||
|
if (args.unassignedTaskRect) {
|
||||||
|
rects.push(args.unassignedTaskRect);
|
||||||
|
}
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
function padCentralCollisionRects(
|
||||||
|
rects: readonly StableRect[],
|
||||||
|
padding: number
|
||||||
|
): StableRect[] {
|
||||||
|
return rects.map((rect) => padRect(rect, padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectOverlapsAnyCentralRect(
|
||||||
|
rect: StableRect,
|
||||||
|
centralCollisionRects: readonly StableRect[]
|
||||||
|
): boolean {
|
||||||
|
return centralCollisionRects.some((centralRect) => rectsOverlap(rect, centralRect));
|
||||||
|
}
|
||||||
|
|
||||||
export function computeOwnerFootprints(
|
export function computeOwnerFootprints(
|
||||||
nodes: GraphNode[],
|
nodes: GraphNode[],
|
||||||
layout?: GraphLayoutPort
|
layout?: GraphLayoutPort
|
||||||
|
|
@ -347,6 +380,7 @@ export function resolveNearestSlotAssignment(args: {
|
||||||
footprintByOwnerId,
|
footprintByOwnerId,
|
||||||
currentFrame,
|
currentFrame,
|
||||||
existingFrames,
|
existingFrames,
|
||||||
|
centralCollisionRects: args.snapshot.centralCollisionRects,
|
||||||
runtimeCentralExclusion: args.snapshot.runtimeCentralExclusion,
|
runtimeCentralExclusion: args.snapshot.runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
pointerX: args.ownerX,
|
pointerX: args.ownerX,
|
||||||
|
|
@ -418,6 +452,9 @@ function validateStaticSnapshotRects(
|
||||||
['leadCentralReservedBlock', snapshot.leadCentralReservedBlock],
|
['leadCentralReservedBlock', snapshot.leadCentralReservedBlock],
|
||||||
['runtimeCentralExclusion', snapshot.runtimeCentralExclusion],
|
['runtimeCentralExclusion', snapshot.runtimeCentralExclusion],
|
||||||
['fitBounds', snapshot.fitBounds],
|
['fitBounds', snapshot.fitBounds],
|
||||||
|
...snapshot.centralCollisionRects.map(
|
||||||
|
(rect, index) => [`centralCollisionRects[${index}]`, rect] as [string, StableRect]
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (snapshot.unassignedTaskRect) {
|
if (snapshot.unassignedTaskRect) {
|
||||||
|
|
@ -452,11 +489,19 @@ function validateLeadSnapshotRects(
|
||||||
if (!rectContainsRect(snapshot.runtimeCentralExclusion, snapshot.leadCentralReservedBlock)) {
|
if (!rectContainsRect(snapshot.runtimeCentralExclusion, snapshot.leadCentralReservedBlock)) {
|
||||||
return { valid: false, reason: 'runtimeCentralExclusion must contain leadCentralReservedBlock' };
|
return { valid: false, reason: 'runtimeCentralExclusion must contain leadCentralReservedBlock' };
|
||||||
}
|
}
|
||||||
|
const paddedCentralCollisionRects = padCentralCollisionRects(
|
||||||
|
snapshot.centralCollisionRects,
|
||||||
|
SLOT_GEOMETRY.centralPadding
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
snapshot.unassignedTaskRect &&
|
paddedCentralCollisionRects.some(
|
||||||
!rectContainsRect(snapshot.runtimeCentralExclusion, snapshot.unassignedTaskRect)
|
(rect) => !rectContainsRect(snapshot.runtimeCentralExclusion, rect)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return { valid: false, reason: 'runtimeCentralExclusion must contain unassignedTaskRect' };
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'runtimeCentralExclusion must contain all centralCollisionRects',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -485,8 +530,11 @@ function validateMemberSlotFrame(
|
||||||
}
|
}
|
||||||
seenAssignments.add(assignmentKey);
|
seenAssignments.add(assignmentKey);
|
||||||
|
|
||||||
if (rectsOverlap(frame.bounds, snapshot.runtimeCentralExclusion)) {
|
if (rectOverlapsAnyCentralRect(frame.bounds, snapshot.centralCollisionRects)) {
|
||||||
return { valid: false, reason: `slot frame for ${frame.ownerId} overlaps runtimeCentralExclusion` };
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: `slot frame for ${frame.ownerId} overlaps centralCollisionRects`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!rectContainsRect(frame.bounds, frame.boardBandRect)) {
|
if (!rectContainsRect(frame.bounds, frame.boardBandRect)) {
|
||||||
return { valid: false, reason: `boardBandRect escapes slot bounds for ${frame.ownerId}` };
|
return { valid: false, reason: `boardBandRect escapes slot bounds for ${frame.ownerId}` };
|
||||||
|
|
@ -614,7 +662,8 @@ function buildUnassignedTaskRect(
|
||||||
|
|
||||||
function planOwnerSlots(
|
function planOwnerSlots(
|
||||||
ownerFootprints: OwnerFootprint[],
|
ownerFootprints: OwnerFootprint[],
|
||||||
centralExclusion: StableRect,
|
centralCollisionRects: readonly StableRect[],
|
||||||
|
runtimeCentralExclusion: StableRect,
|
||||||
layout?: GraphLayoutPort
|
layout?: GraphLayoutPort
|
||||||
): SlotFrame[] {
|
): SlotFrame[] {
|
||||||
const placedFrames: SlotFrame[] = [];
|
const placedFrames: SlotFrame[] = [];
|
||||||
|
|
@ -626,7 +675,8 @@ function planOwnerSlots(
|
||||||
for (const footprint of ownerFootprints) {
|
for (const footprint of ownerFootprints) {
|
||||||
const resolvedFrame = resolveOwnerSlotFrame({
|
const resolvedFrame = resolveOwnerSlotFrame({
|
||||||
footprint,
|
footprint,
|
||||||
centralExclusion,
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
preferredAssignment: preferredAssignments.get(footprint.ownerId),
|
preferredAssignment: preferredAssignments.get(footprint.ownerId),
|
||||||
usedSlotKeys,
|
usedSlotKeys,
|
||||||
|
|
@ -667,7 +717,8 @@ function buildPreferredAssignmentsMap(
|
||||||
|
|
||||||
function resolveOwnerSlotFrame(args: {
|
function resolveOwnerSlotFrame(args: {
|
||||||
footprint: OwnerFootprint;
|
footprint: OwnerFootprint;
|
||||||
centralExclusion: StableRect;
|
centralCollisionRects: readonly StableRect[];
|
||||||
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
preferredAssignment?: GraphOwnerSlotAssignment;
|
preferredAssignment?: GraphOwnerSlotAssignment;
|
||||||
usedSlotKeys: Set<string>;
|
usedSlotKeys: Set<string>;
|
||||||
|
|
@ -676,7 +727,8 @@ function resolveOwnerSlotFrame(args: {
|
||||||
}): SlotFrame {
|
}): SlotFrame {
|
||||||
const {
|
const {
|
||||||
footprint,
|
footprint,
|
||||||
centralExclusion,
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
preferredAssignment,
|
preferredAssignment,
|
||||||
usedSlotKeys,
|
usedSlotKeys,
|
||||||
|
|
@ -690,7 +742,8 @@ function resolveOwnerSlotFrame(args: {
|
||||||
const directMatch = findFirstValidSlotFrame({
|
const directMatch = findFirstValidSlotFrame({
|
||||||
candidateAssignments: candidates,
|
candidateAssignments: candidates,
|
||||||
footprint,
|
footprint,
|
||||||
centralExclusion,
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
usedSlotKeys,
|
usedSlotKeys,
|
||||||
placedFrames,
|
placedFrames,
|
||||||
|
|
@ -706,7 +759,8 @@ function resolveOwnerSlotFrame(args: {
|
||||||
const spilloverMatch = findFirstValidSlotFrame({
|
const spilloverMatch = findFirstValidSlotFrame({
|
||||||
candidateAssignments: spilloverCandidates,
|
candidateAssignments: spilloverCandidates,
|
||||||
footprint,
|
footprint,
|
||||||
centralExclusion,
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
usedSlotKeys,
|
usedSlotKeys,
|
||||||
placedFrames,
|
placedFrames,
|
||||||
|
|
@ -717,7 +771,8 @@ function resolveOwnerSlotFrame(args: {
|
||||||
|
|
||||||
return buildEmergencyFallbackSlotFrame({
|
return buildEmergencyFallbackSlotFrame({
|
||||||
footprint,
|
footprint,
|
||||||
centralExclusion,
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
usedSlotKeys,
|
usedSlotKeys,
|
||||||
placedOwnerCount: placedFrames.length,
|
placedOwnerCount: placedFrames.length,
|
||||||
|
|
@ -728,19 +783,29 @@ function resolveOwnerSlotFrame(args: {
|
||||||
function buildSlotFrame(
|
function buildSlotFrame(
|
||||||
footprint: OwnerFootprint,
|
footprint: OwnerFootprint,
|
||||||
assignment: GraphOwnerSlotAssignment,
|
assignment: GraphOwnerSlotAssignment,
|
||||||
centralExclusion: StableRect,
|
centralCollisionRects: readonly StableRect[],
|
||||||
|
runtimeCentralExclusion: StableRect,
|
||||||
options: { ringStates: RingLayoutStateMap }
|
options: { ringStates: RingLayoutStateMap }
|
||||||
): SlotFrame | null {
|
): SlotFrame | null {
|
||||||
const vector = SECTOR_VECTORS[assignment.sectorIndex % SECTOR_VECTORS.length] ?? SECTOR_VECTORS[0];
|
|
||||||
const radius = resolveRingRadiusForAssignment({
|
const radius = resolveRingRadiusForAssignment({
|
||||||
assignment,
|
assignment,
|
||||||
footprint,
|
footprint,
|
||||||
centralExclusion,
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
ringStates: options.ringStates,
|
ringStates: options.ringStates,
|
||||||
});
|
});
|
||||||
if (radius == null) {
|
if (radius == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return buildSlotFrameAtRadius(footprint, assignment, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSlotFrameAtRadius(
|
||||||
|
footprint: OwnerFootprint,
|
||||||
|
assignment: GraphOwnerSlotAssignment,
|
||||||
|
radius: number
|
||||||
|
): SlotFrame {
|
||||||
|
const vector = SECTOR_VECTORS[assignment.sectorIndex % SECTOR_VECTORS.length] ?? SECTOR_VECTORS[0];
|
||||||
const ownerX = vector.x * radius;
|
const ownerX = vector.x * radius;
|
||||||
const ownerY = vector.y * radius;
|
const ownerY = vector.y * radius;
|
||||||
const slotTop =
|
const slotTop =
|
||||||
|
|
@ -844,7 +909,8 @@ function ownerFootprintsSpillBudget(placedOwnerCount: number): number {
|
||||||
|
|
||||||
function buildEmergencyFallbackSlotFrame(args: {
|
function buildEmergencyFallbackSlotFrame(args: {
|
||||||
footprint: OwnerFootprint;
|
footprint: OwnerFootprint;
|
||||||
centralExclusion: StableRect;
|
centralCollisionRects: readonly StableRect[];
|
||||||
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
usedSlotKeys: Set<string>;
|
usedSlotKeys: Set<string>;
|
||||||
placedOwnerCount: number;
|
placedOwnerCount: number;
|
||||||
|
|
@ -855,9 +921,15 @@ function buildEmergencyFallbackSlotFrame(args: {
|
||||||
sectorIndex: 0,
|
sectorIndex: 0,
|
||||||
};
|
};
|
||||||
args.usedSlotKeys.add(buildAssignmentKey(assignment));
|
args.usedSlotKeys.add(buildAssignmentKey(assignment));
|
||||||
const frame = buildSlotFrame(args.footprint, assignment, args.centralExclusion, {
|
const frame = buildSlotFrame(
|
||||||
ringStates: args.ringStates,
|
args.footprint,
|
||||||
});
|
assignment,
|
||||||
|
args.centralCollisionRects,
|
||||||
|
args.runtimeCentralExclusion,
|
||||||
|
{
|
||||||
|
ringStates: args.ringStates,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
throw new Error(`failed to build emergency fallback slot frame for ${args.footprint.ownerId}`);
|
throw new Error(`failed to build emergency fallback slot frame for ${args.footprint.ownerId}`);
|
||||||
}
|
}
|
||||||
|
|
@ -871,6 +943,7 @@ function rankNearestSlotAssignmentResult(args: {
|
||||||
footprintByOwnerId: ReadonlyMap<string, OwnerFootprint>;
|
footprintByOwnerId: ReadonlyMap<string, OwnerFootprint>;
|
||||||
currentFrame: SlotFrame;
|
currentFrame: SlotFrame;
|
||||||
existingFrames: readonly SlotFrame[];
|
existingFrames: readonly SlotFrame[];
|
||||||
|
centralCollisionRects: readonly StableRect[];
|
||||||
runtimeCentralExclusion: StableRect;
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
pointerX: number;
|
pointerX: number;
|
||||||
|
|
@ -883,14 +956,21 @@ function rankNearestSlotAssignmentResult(args: {
|
||||||
footprintByOwnerId,
|
footprintByOwnerId,
|
||||||
currentFrame,
|
currentFrame,
|
||||||
existingFrames,
|
existingFrames,
|
||||||
|
centralCollisionRects,
|
||||||
runtimeCentralExclusion,
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
} = args;
|
} = args;
|
||||||
const frame = buildSlotFrame(footprint, assignment, runtimeCentralExclusion, {
|
const frame = buildSlotFrame(
|
||||||
ringStates,
|
footprint,
|
||||||
});
|
assignment,
|
||||||
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
|
{
|
||||||
|
ringStates,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -900,6 +980,7 @@ function rankNearestSlotAssignmentResult(args: {
|
||||||
occupiedFrame,
|
occupiedFrame,
|
||||||
footprintByOwnerId,
|
footprintByOwnerId,
|
||||||
currentFrame,
|
currentFrame,
|
||||||
|
centralCollisionRects,
|
||||||
runtimeCentralExclusion,
|
runtimeCentralExclusion,
|
||||||
ringStates,
|
ringStates,
|
||||||
});
|
});
|
||||||
|
|
@ -908,8 +989,8 @@ function rankNearestSlotAssignmentResult(args: {
|
||||||
}
|
}
|
||||||
const otherFrames = existingFrames.filter((existing) => existing.ownerId !== occupiedFrame.ownerId);
|
const otherFrames = existingFrames.filter((existing) => existing.ownerId !== occupiedFrame.ownerId);
|
||||||
if (
|
if (
|
||||||
!isSlotFramePlacementValid(frame, otherFrames, runtimeCentralExclusion) ||
|
!isSlotFramePlacementValid(frame, otherFrames, centralCollisionRects) ||
|
||||||
!isSlotFramePlacementValid(displacedFrame, otherFrames, runtimeCentralExclusion) ||
|
!isSlotFramePlacementValid(displacedFrame, otherFrames, centralCollisionRects) ||
|
||||||
rectsOverlapWithGap(frame.bounds, displacedFrame.bounds, SLOT_GEOMETRY.ringPadding)
|
rectsOverlapWithGap(frame.bounds, displacedFrame.bounds, SLOT_GEOMETRY.ringPadding)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -927,7 +1008,7 @@ function rankNearestSlotAssignmentResult(args: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSlotFramePlacementValid(frame, existingFrames, runtimeCentralExclusion)) {
|
if (!isSlotFramePlacementValid(frame, existingFrames, centralCollisionRects)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -943,6 +1024,7 @@ function buildDisplacedFrameForNearestAssignment(args: {
|
||||||
occupiedFrame: SlotFrame;
|
occupiedFrame: SlotFrame;
|
||||||
footprintByOwnerId: ReadonlyMap<string, OwnerFootprint>;
|
footprintByOwnerId: ReadonlyMap<string, OwnerFootprint>;
|
||||||
currentFrame: SlotFrame;
|
currentFrame: SlotFrame;
|
||||||
|
centralCollisionRects: readonly StableRect[];
|
||||||
runtimeCentralExclusion: StableRect;
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
}): SlotFrame | null {
|
}): SlotFrame | null {
|
||||||
|
|
@ -956,6 +1038,7 @@ function buildDisplacedFrameForNearestAssignment(args: {
|
||||||
ringIndex: args.currentFrame.ringIndex,
|
ringIndex: args.currentFrame.ringIndex,
|
||||||
sectorIndex: args.currentFrame.sectorIndex,
|
sectorIndex: args.currentFrame.sectorIndex,
|
||||||
},
|
},
|
||||||
|
args.centralCollisionRects,
|
||||||
args.runtimeCentralExclusion,
|
args.runtimeCentralExclusion,
|
||||||
{ ringStates: args.ringStates }
|
{ ringStates: args.ringStates }
|
||||||
);
|
);
|
||||||
|
|
@ -982,7 +1065,8 @@ function buildRankedNearestSlotAssignmentResult(args: {
|
||||||
function findFirstValidSlotFrame(args: {
|
function findFirstValidSlotFrame(args: {
|
||||||
candidateAssignments: readonly GraphOwnerSlotAssignment[];
|
candidateAssignments: readonly GraphOwnerSlotAssignment[];
|
||||||
footprint: OwnerFootprint;
|
footprint: OwnerFootprint;
|
||||||
centralExclusion: StableRect;
|
centralCollisionRects: readonly StableRect[];
|
||||||
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
usedSlotKeys: Set<string>;
|
usedSlotKeys: Set<string>;
|
||||||
placedFrames: readonly SlotFrame[];
|
placedFrames: readonly SlotFrame[];
|
||||||
|
|
@ -1000,7 +1084,8 @@ function findFirstValidSlotFrame(args: {
|
||||||
function tryBuildValidSlotFrame(
|
function tryBuildValidSlotFrame(
|
||||||
args: {
|
args: {
|
||||||
footprint: OwnerFootprint;
|
footprint: OwnerFootprint;
|
||||||
centralExclusion: StableRect;
|
centralCollisionRects: readonly StableRect[];
|
||||||
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
usedSlotKeys: Set<string>;
|
usedSlotKeys: Set<string>;
|
||||||
placedFrames: readonly SlotFrame[];
|
placedFrames: readonly SlotFrame[];
|
||||||
|
|
@ -1012,13 +1097,19 @@ function tryBuildValidSlotFrame(
|
||||||
if (args.usedSlotKeys.has(slotKey) && !isSameAssignment(args.preferredAssignment, assignment)) {
|
if (args.usedSlotKeys.has(slotKey) && !isSameAssignment(args.preferredAssignment, assignment)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const frame = buildSlotFrame(args.footprint, assignment, args.centralExclusion, {
|
const frame = buildSlotFrame(
|
||||||
ringStates: args.ringStates,
|
args.footprint,
|
||||||
});
|
assignment,
|
||||||
|
args.centralCollisionRects,
|
||||||
|
args.runtimeCentralExclusion,
|
||||||
|
{
|
||||||
|
ringStates: args.ringStates,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!isSlotFramePlacementValid(frame, args.placedFrames, args.centralExclusion)) {
|
if (!isSlotFramePlacementValid(frame, args.placedFrames, args.centralCollisionRects)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
args.usedSlotKeys.add(slotKey);
|
args.usedSlotKeys.add(slotKey);
|
||||||
|
|
@ -1152,12 +1243,18 @@ function computeSlotDirectionalDepths(
|
||||||
function resolveRingRadiusForAssignment(args: {
|
function resolveRingRadiusForAssignment(args: {
|
||||||
assignment: GraphOwnerSlotAssignment;
|
assignment: GraphOwnerSlotAssignment;
|
||||||
footprint: OwnerFootprint;
|
footprint: OwnerFootprint;
|
||||||
centralExclusion: StableRect;
|
centralCollisionRects: readonly StableRect[];
|
||||||
|
runtimeCentralExclusion: StableRect;
|
||||||
ringStates: RingLayoutStateMap;
|
ringStates: RingLayoutStateMap;
|
||||||
}): number | null {
|
}): number | null {
|
||||||
const vector =
|
const vector =
|
||||||
SECTOR_VECTORS[args.assignment.sectorIndex % SECTOR_VECTORS.length] ?? SECTOR_VECTORS[0];
|
SECTOR_VECTORS[args.assignment.sectorIndex % SECTOR_VECTORS.length] ?? SECTOR_VECTORS[0];
|
||||||
const minRadius = computeMinimumRingRadius(vector, args.footprint, args.centralExclusion);
|
const minRadius = resolveMinimumDirectionalRadius({
|
||||||
|
assignment: args.assignment,
|
||||||
|
footprint: args.footprint,
|
||||||
|
centralCollisionRects: args.centralCollisionRects,
|
||||||
|
runtimeCentralExclusion: args.runtimeCentralExclusion,
|
||||||
|
});
|
||||||
const directionalDepths = computeSlotDirectionalDepths(args.footprint, vector);
|
const directionalDepths = computeSlotDirectionalDepths(args.footprint, vector);
|
||||||
const ringState = resolveVirtualRingState(
|
const ringState = resolveVirtualRingState(
|
||||||
args.assignment.sectorIndex,
|
args.assignment.sectorIndex,
|
||||||
|
|
@ -1211,7 +1308,48 @@ function buildSectorRingStateKey(sectorIndex: number, ringIndex: number): string
|
||||||
return `${sectorIndex}:${ringIndex}`;
|
return `${sectorIndex}:${ringIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeMinimumRingRadius(
|
function resolveMinimumDirectionalRadius(args: {
|
||||||
|
assignment: GraphOwnerSlotAssignment;
|
||||||
|
footprint: OwnerFootprint;
|
||||||
|
centralCollisionRects: readonly StableRect[];
|
||||||
|
runtimeCentralExclusion: StableRect;
|
||||||
|
}): number {
|
||||||
|
const legacyRadiusHint = computeLegacyMinimumRingRadius(
|
||||||
|
SECTOR_VECTORS[args.assignment.sectorIndex % SECTOR_VECTORS.length] ?? SECTOR_VECTORS[0],
|
||||||
|
args.footprint,
|
||||||
|
args.runtimeCentralExclusion
|
||||||
|
);
|
||||||
|
const overlapsCentralCollision = (radius: number): boolean => {
|
||||||
|
const frame = buildSlotFrameAtRadius(args.footprint, args.assignment, radius);
|
||||||
|
return rectOverlapsAnyCentralRect(frame.bounds, args.centralCollisionRects);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!overlapsCentralCollision(0)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let high = Math.max(legacyRadiusHint, SLOT_GEOMETRY.ringGap);
|
||||||
|
let expansionCount = 0;
|
||||||
|
while (overlapsCentralCollision(high) && expansionCount < 24) {
|
||||||
|
low = high;
|
||||||
|
high = Math.max(high * 2, high + SLOT_GEOMETRY.ringGap);
|
||||||
|
expansionCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let iteration = 0; iteration < 24; iteration += 1) {
|
||||||
|
const mid = (low + high) / 2;
|
||||||
|
if (overlapsCentralCollision(mid)) {
|
||||||
|
low = mid;
|
||||||
|
} else {
|
||||||
|
high = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(high);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeLegacyMinimumRingRadius(
|
||||||
vector: { x: number; y: number },
|
vector: { x: number; y: number },
|
||||||
footprint: OwnerFootprint,
|
footprint: OwnerFootprint,
|
||||||
centralExclusion: StableRect
|
centralExclusion: StableRect
|
||||||
|
|
@ -1253,15 +1391,20 @@ function rectsOverlap(a: StableRect, b: StableRect): boolean {
|
||||||
|
|
||||||
function rectContainsRect(outer: StableRect, inner: StableRect): boolean {
|
function rectContainsRect(outer: StableRect, inner: StableRect): boolean {
|
||||||
return (
|
return (
|
||||||
inner.left >= outer.left &&
|
inner.left >= outer.left - GEOMETRY_EPSILON &&
|
||||||
inner.right <= outer.right &&
|
inner.right <= outer.right + GEOMETRY_EPSILON &&
|
||||||
inner.top >= outer.top &&
|
inner.top >= outer.top - GEOMETRY_EPSILON &&
|
||||||
inner.bottom <= outer.bottom
|
inner.bottom <= outer.bottom + GEOMETRY_EPSILON
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointInRect(x: number, y: number, rect: StableRect): boolean {
|
function pointInRect(x: number, y: number, rect: StableRect): boolean {
|
||||||
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
return (
|
||||||
|
x >= rect.left - GEOMETRY_EPSILON &&
|
||||||
|
x <= rect.right + GEOMETRY_EPSILON &&
|
||||||
|
y >= rect.top - GEOMETRY_EPSILON &&
|
||||||
|
y <= rect.bottom + GEOMETRY_EPSILON
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFiniteRect(rect: StableRect): boolean {
|
function isFiniteRect(rect: StableRect): boolean {
|
||||||
|
|
@ -1278,12 +1421,12 @@ function isFiniteRect(rect: StableRect): boolean {
|
||||||
function isSlotFramePlacementValid(
|
function isSlotFramePlacementValid(
|
||||||
frame: SlotFrame,
|
frame: SlotFrame,
|
||||||
existingFrames: readonly SlotFrame[],
|
existingFrames: readonly SlotFrame[],
|
||||||
runtimeCentralExclusion: StableRect
|
centralCollisionRects: readonly StableRect[]
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!isFiniteRect(frame.bounds)) {
|
if (!isFiniteRect(frame.bounds)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (rectsOverlap(frame.bounds, runtimeCentralExclusion)) {
|
if (rectOverlapsAnyCentralRect(frame.bounds, centralCollisionRects)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !existingFrames.some((existing) =>
|
return !existingFrames.some((existing) =>
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,16 @@ import {
|
||||||
computeProcessBandWidth,
|
computeProcessBandWidth,
|
||||||
resolveNearestSlotAssignment,
|
resolveNearestSlotAssignment,
|
||||||
snapshotToWorldBounds,
|
snapshotToWorldBounds,
|
||||||
|
translateSlotFrame,
|
||||||
validateStableSlotLayout,
|
validateStableSlotLayout,
|
||||||
} from '../../../../packages/agent-graph/src/layout/stableSlots';
|
} from '../../../../packages/agent-graph/src/layout/stableSlots';
|
||||||
import { KanbanLayoutEngine } from '../../../../packages/agent-graph/src/layout/kanbanLayout';
|
import { KanbanLayoutEngine } from '../../../../packages/agent-graph/src/layout/kanbanLayout';
|
||||||
import { TASK_PILL } from '../../../../packages/agent-graph/src/constants/canvas-constants';
|
import { TASK_PILL } from '../../../../packages/agent-graph/src/constants/canvas-constants';
|
||||||
import { ACTIVITY_LANE } from '../../../../packages/agent-graph/src/layout/activityLane';
|
import { ACTIVITY_LANE } from '../../../../packages/agent-graph/src/layout/activityLane';
|
||||||
import { STABLE_SLOT_GEOMETRY } from '../../../../packages/agent-graph/src/layout/stableSlotGeometry';
|
import {
|
||||||
|
STABLE_SLOT_GEOMETRY,
|
||||||
|
STABLE_SLOT_SECTOR_VECTORS,
|
||||||
|
} from '../../../../packages/agent-graph/src/layout/stableSlotGeometry';
|
||||||
|
|
||||||
import type { GraphLayoutPort, GraphNode } from '@claude-teams/agent-graph';
|
import type { GraphLayoutPort, GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
|
@ -65,6 +69,18 @@ function createProcess(teamName: string, processId: string, ownerId: string): Gr
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rectsOverlap(
|
||||||
|
left: { left: number; right: number; top: number; bottom: number },
|
||||||
|
right: { left: number; right: number; top: number; bottom: number }
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
left.left < right.right &&
|
||||||
|
left.right > right.left &&
|
||||||
|
left.top < right.bottom &&
|
||||||
|
left.bottom > right.top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe('stable slot layout planner', () => {
|
describe('stable slot layout planner', () => {
|
||||||
it('does not build a stable slot snapshot when the lead is missing', () => {
|
it('does not build a stable slot snapshot when the lead is missing', () => {
|
||||||
const snapshot = buildStableSlotLayoutSnapshot({
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
|
@ -164,6 +180,48 @@ describe('stable slot layout planner', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps diagonal ring-zero sectors closer than the legacy coarse central box radius', () => {
|
||||||
|
const teamName = 'team-directional-radius';
|
||||||
|
const lead = createLead(teamName);
|
||||||
|
const alice = createMember(teamName, 'agent-alice', 'alice');
|
||||||
|
const layout: GraphLayoutPort = {
|
||||||
|
version: 'stable-slots-v1',
|
||||||
|
ownerOrder: [alice.id],
|
||||||
|
slotAssignments: {
|
||||||
|
[alice.id]: { ringIndex: 0, sectorIndex: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, alice],
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
const [footprint] = computeOwnerFootprints([lead, alice], layout);
|
||||||
|
const frame = snapshot?.memberSlotFrames[0];
|
||||||
|
const sectorVector = STABLE_SLOT_SECTOR_VECTORS[1];
|
||||||
|
|
||||||
|
expect(snapshot).not.toBeNull();
|
||||||
|
expect(frame).toBeDefined();
|
||||||
|
expect(footprint).toBeDefined();
|
||||||
|
|
||||||
|
const legacyHorizontalExtent = snapshot!.runtimeCentralExclusion.right;
|
||||||
|
const legacyVerticalExtent = Math.abs(snapshot!.runtimeCentralExclusion.top);
|
||||||
|
const legacyRequiredX =
|
||||||
|
(legacyHorizontalExtent + footprint!.slotWidth / 2 + STABLE_SLOT_GEOMETRY.ringPadding) /
|
||||||
|
Math.abs(sectorVector.x);
|
||||||
|
const legacyRequiredY =
|
||||||
|
(legacyVerticalExtent + footprint!.slotHeight / 2 + STABLE_SLOT_GEOMETRY.ringPadding) /
|
||||||
|
Math.abs(sectorVector.y);
|
||||||
|
const legacyMinRadius = Math.max(legacyRequiredX, legacyRequiredY, 0);
|
||||||
|
const actualRadius = Math.abs(frame!.ownerX / sectorVector.x);
|
||||||
|
|
||||||
|
expect(actualRadius).toBeLessThan(legacyMinRadius);
|
||||||
|
expect(
|
||||||
|
snapshot!.centralCollisionRects.some((rect) => rectsOverlap(frame!.bounds, rect))
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('grows process band width when an owner has multiple visible process nodes', () => {
|
it('grows process band width when an owner has multiple visible process nodes', () => {
|
||||||
const teamName = 'team-process-growth';
|
const teamName = 'team-process-growth';
|
||||||
const lead = createLead(teamName);
|
const lead = createLead(teamName);
|
||||||
|
|
@ -251,6 +309,51 @@ describe('stable slot layout planner', () => {
|
||||||
expect(validateStableSlotLayout(invalid).valid).toBe(false);
|
expect(validateStableSlotLayout(invalid).valid).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects member frames that overlap lead activity and launch central collision rects', () => {
|
||||||
|
const teamName = 'team-central-rects';
|
||||||
|
const lead = createLead(teamName);
|
||||||
|
const alice = createMember(teamName, 'agent-alice', 'alice');
|
||||||
|
const layout: GraphLayoutPort = {
|
||||||
|
version: 'stable-slots-v1',
|
||||||
|
ownerOrder: [alice.id],
|
||||||
|
slotAssignments: {
|
||||||
|
[alice.id]: { ringIndex: 0, sectorIndex: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, alice],
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot).not.toBeNull();
|
||||||
|
const [frame] = snapshot!.memberSlotFrames;
|
||||||
|
const overlappingLeadActivity = translateSlotFrame(
|
||||||
|
frame,
|
||||||
|
snapshot!.leadActivityRect.left - frame.bounds.left + 1,
|
||||||
|
snapshot!.leadActivityRect.top - frame.bounds.top + 1
|
||||||
|
);
|
||||||
|
const overlappingLaunchHud = translateSlotFrame(
|
||||||
|
frame,
|
||||||
|
snapshot!.launchHudRect.left - frame.bounds.left + 1,
|
||||||
|
snapshot!.launchHudRect.top - frame.bounds.top + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
validateStableSlotLayout({
|
||||||
|
...snapshot!,
|
||||||
|
memberSlotFrames: [overlappingLeadActivity],
|
||||||
|
}).valid
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
validateStableSlotLayout({
|
||||||
|
...snapshot!,
|
||||||
|
memberSlotFrames: [overlappingLaunchHud],
|
||||||
|
}).valid
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('prefers the occupied target slot when dragging near another owner anchor', () => {
|
it('prefers the occupied target slot when dragging near another owner anchor', () => {
|
||||||
const teamName = 'team-b';
|
const teamName = 'team-b';
|
||||||
const lead = createLead(teamName);
|
const lead = createLead(teamName);
|
||||||
|
|
@ -290,6 +393,64 @@ describe('stable slot layout planner', () => {
|
||||||
expect(nearest?.displacedAssignment).toEqual({ ringIndex: 0, sectorIndex: 1 });
|
expect(nearest?.displacedAssignment).toEqual({ ringIndex: 0, sectorIndex: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps nearest-slot drag resolution on the same central collision model as the planner', () => {
|
||||||
|
const teamName = 'team-drag-central-collision';
|
||||||
|
const lead = createLead(teamName);
|
||||||
|
const alice = createMember(teamName, 'agent-alice', 'alice');
|
||||||
|
const bob = createMember(teamName, 'agent-bob', 'bob');
|
||||||
|
const layout: GraphLayoutPort = {
|
||||||
|
version: 'stable-slots-v1',
|
||||||
|
ownerOrder: [alice.id, bob.id],
|
||||||
|
slotAssignments: {
|
||||||
|
[alice.id]: { ringIndex: 0, sectorIndex: 1 },
|
||||||
|
[bob.id]: { ringIndex: 0, sectorIndex: 2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, alice, bob],
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot).not.toBeNull();
|
||||||
|
const nearest = resolveNearestSlotAssignment({
|
||||||
|
ownerId: alice.id,
|
||||||
|
ownerX: snapshot!.leadActivityRect.left + snapshot!.leadActivityRect.width / 2,
|
||||||
|
ownerY: snapshot!.leadActivityRect.top + snapshot!.leadActivityRect.height / 2,
|
||||||
|
nodes: [lead, alice, bob],
|
||||||
|
snapshot: snapshot!,
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nearest).not.toBeNull();
|
||||||
|
const replannedSnapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, alice, bob],
|
||||||
|
layout: {
|
||||||
|
...layout,
|
||||||
|
slotAssignments: {
|
||||||
|
...layout.slotAssignments,
|
||||||
|
[alice.id]: nearest!.assignment,
|
||||||
|
...(nearest?.displacedOwnerId && nearest.displacedAssignment
|
||||||
|
? { [nearest.displacedOwnerId]: nearest.displacedAssignment }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const replannedFrame = replannedSnapshot?.memberSlotFrames.find(
|
||||||
|
(frame) => frame.ownerId === alice.id
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(replannedSnapshot).not.toBeNull();
|
||||||
|
expect(replannedFrame).toBeDefined();
|
||||||
|
expect(
|
||||||
|
replannedSnapshot!.centralCollisionRects.some((rect) =>
|
||||||
|
rectsOverlap(replannedFrame!.bounds, rect)
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('treats tasks with missing owner nodes as unassigned topology actors', () => {
|
it('treats tasks with missing owner nodes as unassigned topology actors', () => {
|
||||||
const teamName = 'team-orphan-task';
|
const teamName = 'team-orphan-task';
|
||||||
const lead = createLead(teamName);
|
const lead = createLead(teamName);
|
||||||
|
|
@ -313,6 +474,45 @@ describe('stable slot layout planner', () => {
|
||||||
expect(snapshot?.unassignedTaskRect).not.toBeNull();
|
expect(snapshot?.unassignedTaskRect).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects member frames that overlap the unassigned central collision rect', () => {
|
||||||
|
const teamName = 'team-unassigned-central-rect';
|
||||||
|
const lead = createLead(teamName);
|
||||||
|
const alice = createMember(teamName, 'agent-alice', 'alice');
|
||||||
|
const orphanTask = createTask(
|
||||||
|
teamName,
|
||||||
|
'task-orphan',
|
||||||
|
'member:team-unassigned-central-rect:ghost'
|
||||||
|
);
|
||||||
|
const layout: GraphLayoutPort = {
|
||||||
|
version: 'stable-slots-v1',
|
||||||
|
ownerOrder: [alice.id],
|
||||||
|
slotAssignments: {
|
||||||
|
[alice.id]: { ringIndex: 0, sectorIndex: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, alice, orphanTask],
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot?.unassignedTaskRect).not.toBeNull();
|
||||||
|
const [frame] = snapshot!.memberSlotFrames;
|
||||||
|
const overlappingUnassigned = translateSlotFrame(
|
||||||
|
frame,
|
||||||
|
snapshot!.unassignedTaskRect!.left - frame.bounds.left + 1,
|
||||||
|
snapshot!.unassignedTaskRect!.top - frame.bounds.top + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
validateStableSlotLayout({
|
||||||
|
...snapshot!,
|
||||||
|
memberSlotFrames: [overlappingUnassigned],
|
||||||
|
}).valid
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('computes the next ring radius from previous ring depth, not member count', () => {
|
it('computes the next ring radius from previous ring depth, not member count', () => {
|
||||||
const teamName = 'team-ring-depth';
|
const teamName = 'team-ring-depth';
|
||||||
const lead = createLead(teamName);
|
const lead = createLead(teamName);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue