feat: лассо-выделение по кнопке, разнесение портов карт/устройств, удвоение мока
All checks were successful
continuous-integration/drone/push Build is passing

- Лассо (rubberband) отключено по умолчанию, включается кнопкой в тулбаре
- Порты устройств с картами позиционируются ниже карт (absolute positioning)
- Высота устройств с картами и портами: сумма вместо max
- Мок данные удвоены: +4 сайта, +26 устройств, +6 карт, ~130 портов, ~30 линий

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alina
2026-02-17 23:45:18 +03:00
parent 3d9a25feac
commit 323410ead7
7 changed files with 726 additions and 6 deletions

View File

@ -166,7 +166,7 @@ export function initGraph(
new Selection({
enabled: true,
multiple: true,
rubberband: true,
rubberband: false,
movable: true,
showNodeSelectionBox: true,
}),

View File

@ -53,7 +53,10 @@ function getDeviceSize(
}
const portHeight = Math.max(portCount * 22, 60);
const cardsHeight = cardCount > 0 ? cardCount * (CARD_HEIGHT + 6) + 8 : 0;
const bodyHeight = Math.max(portHeight, cardsHeight);
// When device has both cards and ports, stack them vertically to avoid overlap
const bodyHeight = cardCount > 0 && portCount > 0
? cardsHeight + portHeight
: Math.max(portHeight, cardsHeight);
return {
width: DEVICE_MIN_WIDTH,
height: Math.max(DEVICE_MIN_HEIGHT, DEVICE_HEADER_HEIGHT + bodyHeight + 10),
@ -112,12 +115,42 @@ export function buildGraphData(
const deviceCards = data.cards.filter((c) => c.deviceId === device.id && c.visible);
const size = getDeviceSize(device.category, device.name, device.marking, devicePorts.length, deviceCards.length);
// When device has cards, position device-level ports below cards area
const hasCards = deviceCards.length > 0;
const cardsEndY = hasCards
? DEVICE_HEADER_HEIGHT + deviceCards.length * (CARD_HEIGHT + 6) + 8
: 0;
// Group device ports by resolved side for Y offset calculation
const leftDevicePorts = devicePorts.filter(
(p) => (portSideMap.get(p.id) ?? p.side) === 'left',
);
const rightDevicePorts = devicePorts.filter(
(p) => (portSideMap.get(p.id) ?? p.side) === 'right',
);
const portItems = devicePorts.map((port) => {
const resolvedSide = portSideMap.get(port.id) ?? port.side;
const label = port.slotName ? `${port.slotName}:${port.name}` : port.name;
return createPortItem(port.id, resolvedSide, label, port.labelColor || undefined);
let portArgs: { x: number; y: number } | undefined;
if (hasCards) {
const sameSidePorts = resolvedSide === 'left' ? leftDevicePorts : rightDevicePorts;
const indexInSide = sameSidePorts.indexOf(port);
const portSpacing = 22;
portArgs = {
x: resolvedSide === 'left' ? 0 : size.width,
y: cardsEndY + portSpacing * (indexInSide + 0.5),
};
}
return createPortItem(port.id, resolvedSide, label, port.labelColor || undefined, portArgs);
});
// Use absolute positioning when device has cards to avoid overlap with card ports
const leftPosition = hasCards ? 'absolute' : 'left';
const rightPosition = hasCards ? 'absolute' : 'right';
const node: GraphNodeConfig = {
id: device.id,
shape,
@ -141,7 +174,7 @@ export function buildGraphData(
ports: {
groups: {
left: {
position: 'left',
position: leftPosition,
attrs: {
circle: {
r: 6,
@ -156,7 +189,7 @@ export function buildGraphData(
},
},
right: {
position: 'right',
position: rightPosition,
attrs: {
circle: {
r: 6,

View File

@ -38,7 +38,10 @@ function getDeviceSize(device: Device, portCount: number, cardCount: number): {
// Dynamic height based on port count + header + cards
const portHeight = Math.max(portCount * 22, 60);
const cardsHeight = cardCount > 0 ? cardCount * (CARD_HEIGHT + 6) + 8 : 0;
const bodyHeight = Math.max(portHeight, cardsHeight);
// When device has both cards and ports, stack them vertically to avoid overlap
const bodyHeight = cardCount > 0 && portCount > 0
? cardsHeight + portHeight
: Math.max(portHeight, cardsHeight);
return {
width: DEVICE_MIN_WIDTH,
height: Math.max(DEVICE_MIN_HEIGHT, DEVICE_HEADER_HEIGHT + bodyHeight + 10),

View File

@ -44,10 +44,12 @@ export function createPortItem(
side: 'left' | 'right',
label: string,
labelColor?: string,
args?: { x: number; y: number },
) {
return {
id: portId,
group: side,
args,
attrs: {
text: {
text: label,