mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-23 22:48:57 -05:00
added massive overhaul of connector labels, now you can add as many as you like! as long as its below 256, you can do per line, in double connector mode, Fixes #107
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Connector, connectorStyleOptions, connectorLineTypeOptions } from 'src/types';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
Connector,
|
||||
ConnectorLabel,
|
||||
connectorStyleOptions,
|
||||
connectorLineTypeOptions
|
||||
} from 'src/types';
|
||||
import {
|
||||
Box,
|
||||
Slider,
|
||||
@@ -9,14 +14,21 @@ import {
|
||||
IconButton as MUIIconButton,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Typography
|
||||
Typography,
|
||||
Button,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import { useConnector } from 'src/hooks/useConnector';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { ColorPicker } from 'src/components/ColorSelector/ColorPicker';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon
|
||||
} from '@mui/icons-material';
|
||||
import { getConnectorLabels, generateId } from 'src/utils';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Section } from '../components/Section';
|
||||
import { DeleteButton } from '../components/DeleteButton';
|
||||
@@ -31,16 +43,91 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
});
|
||||
const connector = useConnector(id);
|
||||
const { updateConnector, deleteConnector } = useScene();
|
||||
const [useCustomColor, setUseCustomColor] = useState(!!connector?.customColor);
|
||||
const [useCustomColor, setUseCustomColor] = useState(
|
||||
!!connector?.customColor
|
||||
);
|
||||
|
||||
// Get all labels (including migrated legacy labels)
|
||||
const labels = useMemo(() => {
|
||||
if (!connector) return [];
|
||||
return getConnectorLabels(connector);
|
||||
}, [connector]);
|
||||
|
||||
// If connector doesn't exist, return null
|
||||
if (!connector) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDoubleLineType =
|
||||
connector.lineType === 'DOUBLE' ||
|
||||
connector.lineType === 'DOUBLE_WITH_CIRCLE';
|
||||
|
||||
const handleAddLabel = () => {
|
||||
if (labels.length >= 256) return;
|
||||
|
||||
const newLabel: ConnectorLabel = {
|
||||
id: generateId(),
|
||||
text: '',
|
||||
position: 50,
|
||||
height: 0,
|
||||
line: '1'
|
||||
};
|
||||
|
||||
// Migrate legacy labels if needed and add new label
|
||||
const updatedLabels = [...labels, newLabel];
|
||||
updateConnector(connector.id, {
|
||||
labels: updatedLabels,
|
||||
// Clear legacy fields on first new label addition
|
||||
description: undefined,
|
||||
startLabel: undefined,
|
||||
endLabel: undefined,
|
||||
startLabelHeight: undefined,
|
||||
centerLabelHeight: undefined,
|
||||
endLabelHeight: undefined
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateLabel = (
|
||||
labelId: string,
|
||||
updates: Partial<ConnectorLabel>
|
||||
) => {
|
||||
const updatedLabels = labels.map((label) => {
|
||||
return label.id === labelId ? { ...label, ...updates } : label;
|
||||
});
|
||||
|
||||
updateConnector(connector.id, {
|
||||
labels: updatedLabels,
|
||||
// Clear legacy fields
|
||||
description: undefined,
|
||||
startLabel: undefined,
|
||||
endLabel: undefined,
|
||||
startLabelHeight: undefined,
|
||||
centerLabelHeight: undefined,
|
||||
endLabelHeight: undefined
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteLabel = (labelId: string) => {
|
||||
const updatedLabels = labels.filter((label) => {
|
||||
return label.id !== labelId;
|
||||
});
|
||||
updateConnector(connector.id, {
|
||||
labels: updatedLabels,
|
||||
// Clear legacy fields
|
||||
description: undefined,
|
||||
startLabel: undefined,
|
||||
endLabel: undefined,
|
||||
startLabelHeight: undefined,
|
||||
centerLabelHeight: undefined,
|
||||
endLabelHeight: undefined
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Box sx={{ position: 'relative', paddingTop: '24px', paddingBottom: '24px' }}>
|
||||
<Box
|
||||
sx={{ position: 'relative', paddingTop: '24px', paddingBottom: '24px' }}
|
||||
>
|
||||
{/* Close button */}
|
||||
<MUIIconButton
|
||||
aria-label="Close"
|
||||
@@ -57,79 +144,160 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
>
|
||||
<CloseIcon />
|
||||
</MUIIconButton>
|
||||
<Section>
|
||||
<TextField
|
||||
label="Center Label (Description)"
|
||||
value={connector.description}
|
||||
onChange={(e) => {
|
||||
updateConnector(connector.id, {
|
||||
description: e.target.value as string
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Start Label"
|
||||
value={connector.startLabel || ''}
|
||||
onChange={(e) => {
|
||||
updateConnector(connector.id, {
|
||||
startLabel: e.target.value as string
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="End Label"
|
||||
value={connector.endLabel || ''}
|
||||
onChange={(e) => {
|
||||
updateConnector(connector.id, {
|
||||
endLabel: e.target.value as string
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Label Heights">
|
||||
<Section title="Labels">
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">Start Label Height</Typography>
|
||||
<Slider
|
||||
marks
|
||||
step={10}
|
||||
min={-100}
|
||||
max={100}
|
||||
value={connector.startLabelHeight || 0}
|
||||
onChange={(e, value) => {
|
||||
updateConnector(connector.id, { startLabelHeight: value as number });
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 2
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">Center Label Height</Typography>
|
||||
<Slider
|
||||
marks
|
||||
step={10}
|
||||
min={-100}
|
||||
max={100}
|
||||
value={connector.centerLabelHeight || 0}
|
||||
onChange={(e, value) => {
|
||||
updateConnector(connector.id, { centerLabelHeight: value as number });
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary">End Label Height</Typography>
|
||||
<Slider
|
||||
marks
|
||||
step={10}
|
||||
min={-100}
|
||||
max={100}
|
||||
value={connector.endLabelHeight || 0}
|
||||
onChange={(e, value) => {
|
||||
updateConnector(connector.id, { endLabelHeight: value as number });
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{labels.length} / 256 labels
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAddLabel}
|
||||
disabled={labels.length >= 256}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
Add Label
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{labels.length === 0 && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', py: 2 }}
|
||||
>
|
||||
No labels. Click "Add Label" to create one.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{labels.map((label, index) => {
|
||||
return (
|
||||
<Paper key={label.id} variant="outlined" sx={{ p: 2, mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Label {index + 1}
|
||||
</Typography>
|
||||
<MUIIconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
return handleDeleteLabel(label.id);
|
||||
}}
|
||||
color="error"
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</MUIIconButton>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
label="Text"
|
||||
value={label.text}
|
||||
onChange={(e) => {
|
||||
return handleUpdateLabel(label.id, {
|
||||
text: e.target.value
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
|
||||
<TextField
|
||||
label="Position (%)"
|
||||
type="number"
|
||||
value={label.position}
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Allow empty input
|
||||
if (inputValue === '') {
|
||||
handleUpdateLabel(label.id, { position: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const value = parseInt(inputValue, 10);
|
||||
if (!Number.isNaN(value)) {
|
||||
handleUpdateLabel(label.id, {
|
||||
position: Math.max(0, Math.min(100, value))
|
||||
});
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// On blur, ensure we have a valid value
|
||||
if (e.target.value === '') {
|
||||
handleUpdateLabel(label.id, { position: 0 });
|
||||
}
|
||||
}}
|
||||
inputProps={{ min: 0, max: 100 }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
|
||||
{isDoubleLineType && (
|
||||
<Select
|
||||
value={label.line || '1'}
|
||||
onChange={(e) => {
|
||||
return handleUpdateLabel(label.id, {
|
||||
line: e.target.value as '1' | '2'
|
||||
});
|
||||
}}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
<MenuItem value="1">Line 1</MenuItem>
|
||||
<MenuItem value="2">Line 2</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Height Offset
|
||||
</Typography>
|
||||
<Slider
|
||||
marks
|
||||
step={10}
|
||||
min={-100}
|
||||
max={100}
|
||||
value={label.height || 0}
|
||||
onChange={(e, value) => {
|
||||
return handleUpdateLabel(label.id, {
|
||||
height: value as number
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={label.showLine !== false}
|
||||
onChange={(e) => {
|
||||
return handleUpdateLabel(label.id, {
|
||||
showLine: e.target.checked
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Show Dotted Line"
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Section>
|
||||
<Section title="Color">
|
||||
@@ -163,7 +331,10 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
) : (
|
||||
<ColorSelector
|
||||
onChange={(color) => {
|
||||
return updateConnector(connector.id, { color, customColor: '' });
|
||||
return updateConnector(connector.id, {
|
||||
color,
|
||||
customColor: ''
|
||||
});
|
||||
}}
|
||||
activeColor={connector.color}
|
||||
/>
|
||||
@@ -193,7 +364,11 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
{Object.values(connectorStyleOptions).map((style) => {
|
||||
return <MenuItem key={style} value={style}>{style}</MenuItem>;
|
||||
return (
|
||||
<MenuItem key={style} value={style}>
|
||||
{style}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Section>
|
||||
@@ -208,10 +383,17 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
fullWidth
|
||||
>
|
||||
{Object.values(connectorLineTypeOptions).map((type) => {
|
||||
const displayName = type === 'SINGLE' ? 'Single Line' :
|
||||
type === 'DOUBLE' ? 'Double Line' :
|
||||
'Double Line with Circle';
|
||||
return <MenuItem key={type} value={type}>{displayName}</MenuItem>;
|
||||
let displayName = 'Double Line with Circle';
|
||||
if (type === 'SINGLE') {
|
||||
displayName = 'Single Line';
|
||||
} else if (type === 'DOUBLE') {
|
||||
displayName = 'Double Line';
|
||||
}
|
||||
return (
|
||||
<MenuItem key={type} value={type}>
|
||||
{displayName}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Props {
|
||||
expandDirection?: 'CENTER' | 'BOTTOM';
|
||||
children: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
showLine?: boolean;
|
||||
}
|
||||
|
||||
export const Label = ({
|
||||
@@ -18,7 +19,8 @@ export const Label = ({
|
||||
maxHeight,
|
||||
expandDirection = 'CENTER',
|
||||
labelHeight = 0,
|
||||
sx
|
||||
sx,
|
||||
showLine = true
|
||||
}: Props) => {
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
|
||||
@@ -29,7 +31,7 @@ export const Label = ({
|
||||
width: maxWidth
|
||||
}}
|
||||
>
|
||||
{labelHeight > 0 && (
|
||||
{labelHeight > 0 && showLine && (
|
||||
<Box
|
||||
component="svg"
|
||||
viewBox={`0 0 ${CONNECTOR_DOT_SIZE} ${labelHeight}`}
|
||||
|
||||
@@ -1,131 +1,126 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { connectorPathTileToGlobal, getTilePosition } from 'src/utils';
|
||||
import { PROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { useConnector } from 'src/hooks/useConnector';
|
||||
import {
|
||||
connectorPathTileToGlobal,
|
||||
getTilePosition,
|
||||
getConnectorLabels,
|
||||
getLabelTileIndex
|
||||
} from 'src/utils';
|
||||
import { PROJECTED_TILE_SIZE, UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { Label } from 'src/components/Label/Label';
|
||||
import { ConnectorLabel as ConnectorLabelType } from 'src/types';
|
||||
|
||||
interface Props {
|
||||
connector: ReturnType<typeof useScene>['connectors'][0];
|
||||
}
|
||||
|
||||
export const ConnectorLabel = ({ connector }: Props) => {
|
||||
const centerLabelPosition = useMemo(() => {
|
||||
const tileIndex = Math.floor(connector.path.tiles.length / 2);
|
||||
const tile = connector.path.tiles[tileIndex];
|
||||
export const ConnectorLabel = ({ connector: sceneConnector }: Props) => {
|
||||
const connector = useConnector(sceneConnector.id);
|
||||
|
||||
return getTilePosition({
|
||||
tile: connectorPathTileToGlobal(tile, connector.path.rectangle.from)
|
||||
});
|
||||
}, [connector.path]);
|
||||
const labels = useMemo(() => {
|
||||
if (!connector) return [];
|
||||
return getConnectorLabels(connector);
|
||||
}, [connector]);
|
||||
|
||||
const startLabelPosition = useMemo(() => {
|
||||
if (!connector.startLabel) return null;
|
||||
const tiles = connector.path.tiles;
|
||||
if (tiles.length < 2) return null;
|
||||
|
||||
// Use second tile position for start label to offset from node
|
||||
const tile = tiles[Math.min(1, tiles.length - 1)];
|
||||
return getTilePosition({
|
||||
tile: connectorPathTileToGlobal(tile, connector.path.rectangle.from)
|
||||
});
|
||||
}, [connector.path, connector.startLabel]);
|
||||
// Calculate label positions based on percentage and line assignment
|
||||
const labelPositions = useMemo(() => {
|
||||
if (!connector) return [];
|
||||
|
||||
const endLabelPosition = useMemo(() => {
|
||||
if (!connector.endLabel) return null;
|
||||
const tiles = connector.path.tiles;
|
||||
if (tiles.length < 2) return null;
|
||||
|
||||
// Use second-to-last tile position for end label to offset from node
|
||||
const tile = tiles[Math.max(tiles.length - 2, 0)];
|
||||
return getTilePosition({
|
||||
tile: connectorPathTileToGlobal(tile, connector.path.rectangle.from)
|
||||
});
|
||||
}, [connector.path, connector.endLabel]);
|
||||
|
||||
return labels
|
||||
.map((label) => {
|
||||
const tileIndex = getLabelTileIndex(
|
||||
sceneConnector.path.tiles.length,
|
||||
label.position
|
||||
);
|
||||
const tile = sceneConnector.path.tiles[tileIndex];
|
||||
|
||||
if (!tile) return null;
|
||||
|
||||
let position = getTilePosition({
|
||||
tile: connectorPathTileToGlobal(
|
||||
tile,
|
||||
sceneConnector.path.rectangle.from
|
||||
)
|
||||
});
|
||||
|
||||
// For double line types, offset labels based on line assignment
|
||||
const lineType = connector.lineType || 'SINGLE';
|
||||
if (
|
||||
(lineType === 'DOUBLE' || lineType === 'DOUBLE_WITH_CIRCLE') &&
|
||||
label.line === '2'
|
||||
) {
|
||||
// Calculate offset perpendicular to line direction
|
||||
const { tiles } = sceneConnector.path;
|
||||
if (tileIndex > 0 && tileIndex < tiles.length - 1) {
|
||||
const prev = tiles[tileIndex - 1];
|
||||
const next = tiles[tileIndex + 1];
|
||||
const dx = next.x - prev.x;
|
||||
const dy = next.y - prev.y;
|
||||
const len = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||
|
||||
// Perpendicular offset (matches the offset in Connector.tsx)
|
||||
const connectorWidthPx =
|
||||
(UNPROJECTED_TILE_SIZE / 100) * (connector.width || 15);
|
||||
const offset = connectorWidthPx * 3;
|
||||
const perpX = -dy / len;
|
||||
const perpY = dx / len;
|
||||
|
||||
position = {
|
||||
x: position.x - perpX * offset,
|
||||
y: position.y - perpY * offset
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { label, position };
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
item
|
||||
): item is {
|
||||
label: ConnectorLabelType;
|
||||
position: { x: number; y: number };
|
||||
} => {
|
||||
return item !== null;
|
||||
}
|
||||
);
|
||||
}, [labels, sceneConnector.path, connector?.lineType, connector?.width]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Center label (existing description) */}
|
||||
{connector.description && (
|
||||
<Box
|
||||
sx={{ position: 'absolute', pointerEvents: 'none' }}
|
||||
style={{
|
||||
maxWidth: PROJECTED_TILE_SIZE.width,
|
||||
left: centerLabelPosition.x,
|
||||
top: centerLabelPosition.y
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
maxWidth={150}
|
||||
labelHeight={connector.centerLabelHeight || 0}
|
||||
sx={{
|
||||
py: 0.75,
|
||||
px: 1,
|
||||
borderRadius: 2
|
||||
{labelPositions.map(({ label, position }) => {
|
||||
return (
|
||||
<Box
|
||||
key={label.id}
|
||||
sx={{ position: 'absolute', pointerEvents: 'none' }}
|
||||
style={{
|
||||
maxWidth: PROJECTED_TILE_SIZE.width,
|
||||
left: position.x,
|
||||
top: position.y
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{connector.description}
|
||||
</Typography>
|
||||
</Label>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Start label */}
|
||||
{connector.startLabel && startLabelPosition && (
|
||||
<Box
|
||||
sx={{ position: 'absolute', pointerEvents: 'none' }}
|
||||
style={{
|
||||
maxWidth: PROJECTED_TILE_SIZE.width,
|
||||
left: startLabelPosition.x,
|
||||
top: startLabelPosition.y
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
maxWidth={100}
|
||||
labelHeight={connector.startLabelHeight || 0}
|
||||
sx={{
|
||||
py: 0.5,
|
||||
px: 0.75,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'background.paper',
|
||||
opacity: 0.9
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{connector.startLabel}
|
||||
</Typography>
|
||||
</Label>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* End label */}
|
||||
{connector.endLabel && endLabelPosition && (
|
||||
<Box
|
||||
sx={{ position: 'absolute', pointerEvents: 'none' }}
|
||||
style={{
|
||||
maxWidth: PROJECTED_TILE_SIZE.width,
|
||||
left: endLabelPosition.x,
|
||||
top: endLabelPosition.y
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
maxWidth={100}
|
||||
labelHeight={connector.endLabelHeight || 0}
|
||||
sx={{
|
||||
py: 0.5,
|
||||
px: 0.75,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'background.paper',
|
||||
opacity: 0.9
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{connector.endLabel}
|
||||
</Typography>
|
||||
</Label>
|
||||
</Box>
|
||||
)}
|
||||
<Label
|
||||
maxWidth={150}
|
||||
labelHeight={label.height || 0}
|
||||
showLine={label.showLine !== false}
|
||||
sx={{
|
||||
py: 0.75,
|
||||
px: 1,
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
opacity: 0.95
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{label.text}
|
||||
</Typography>
|
||||
</Label>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,12 @@ export const ConnectorLabels = ({ connectors }: Props) => {
|
||||
<>
|
||||
{connectors
|
||||
.filter((connector) => {
|
||||
return Boolean(connector.description || connector.startLabel || connector.endLabel);
|
||||
return Boolean(
|
||||
connector.description ||
|
||||
connector.startLabel ||
|
||||
connector.endLabel ||
|
||||
(connector.labels && connector.labels.length > 0)
|
||||
);
|
||||
})
|
||||
.map((connector) => {
|
||||
return <ConnectorLabel key={connector.id} connector={connector} />;
|
||||
|
||||
@@ -53,6 +53,7 @@ export const CONNECTOR_DEFAULTS: Required<Omit<Connector, 'id' | 'color'>> = {
|
||||
startLabelHeight: 0,
|
||||
centerLabelHeight: 0,
|
||||
endLabelHeight: 0,
|
||||
labels: [],
|
||||
customColor: '',
|
||||
anchors: [],
|
||||
style: 'SOLID',
|
||||
|
||||
@@ -4,6 +4,15 @@ import { coords, id, constrainedStrings } from './common';
|
||||
export const connectorStyleOptions = ['SOLID', 'DOTTED', 'DASHED'] as const;
|
||||
export const connectorLineTypeOptions = ['SINGLE', 'DOUBLE', 'DOUBLE_WITH_CIRCLE'] as const;
|
||||
|
||||
export const connectorLabelSchema = z.object({
|
||||
id,
|
||||
text: constrainedStrings.description,
|
||||
position: z.number().min(0).max(100), // Percentage along the path (0-100)
|
||||
height: z.number().optional(), // Vertical offset
|
||||
line: z.enum(['1', '2']).optional(), // Which line for double line types (defaults to '1')
|
||||
showLine: z.boolean().optional() // Show the dotted line connecting label to connector (defaults to true)
|
||||
});
|
||||
|
||||
export const anchorSchema = z.object({
|
||||
id,
|
||||
ref: z
|
||||
@@ -17,12 +26,15 @@ export const anchorSchema = z.object({
|
||||
|
||||
export const connectorSchema = z.object({
|
||||
id,
|
||||
// Legacy label fields (for backward compatibility)
|
||||
description: constrainedStrings.description.optional(),
|
||||
startLabel: constrainedStrings.description.optional(),
|
||||
endLabel: constrainedStrings.description.optional(),
|
||||
startLabelHeight: z.number().optional(),
|
||||
centerLabelHeight: z.number().optional(),
|
||||
endLabelHeight: z.number().optional(),
|
||||
// New flexible labels array
|
||||
labels: z.array(connectorLabelSchema).max(256).optional(),
|
||||
color: id.optional(),
|
||||
customColor: z.string().optional(), // For custom RGB colors
|
||||
width: z.number().optional(),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
viewSchema,
|
||||
viewItemSchema,
|
||||
connectorSchema,
|
||||
connectorLabelSchema,
|
||||
iconsSchema,
|
||||
colorsSchema,
|
||||
anchorSchema,
|
||||
@@ -31,6 +32,7 @@ export type ViewItem = z.infer<typeof viewItemSchema>;
|
||||
export type ConnectorStyle = keyof typeof connectorStyleOptions;
|
||||
export type ConnectorLineType = keyof typeof connectorLineTypeOptions;
|
||||
export type ConnectorAnchor = z.infer<typeof anchorSchema>;
|
||||
export type ConnectorLabel = z.infer<typeof connectorLabelSchema>;
|
||||
export type Connector = z.infer<typeof connectorSchema>;
|
||||
export type TextBox = z.infer<typeof textBoxSchema>;
|
||||
export type Rectangle = z.infer<typeof rectangleSchema>;
|
||||
|
||||
71
packages/fossflow-lib/src/utils/connectorLabels.ts
Normal file
71
packages/fossflow-lib/src/utils/connectorLabels.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Connector, ConnectorLabel } from 'src/types';
|
||||
import { generateId } from './common';
|
||||
|
||||
/**
|
||||
* Migrates legacy connector labels (description, startLabel, endLabel)
|
||||
* to the new flexible labels array format
|
||||
*/
|
||||
export const migrateLegacyLabels = (connector: Connector): ConnectorLabel[] => {
|
||||
const labels: ConnectorLabel[] = [];
|
||||
|
||||
// Convert startLabel to 10% position
|
||||
if (connector.startLabel) {
|
||||
labels.push({
|
||||
id: generateId(),
|
||||
text: connector.startLabel,
|
||||
position: 10,
|
||||
height: connector.startLabelHeight,
|
||||
line: '1'
|
||||
});
|
||||
}
|
||||
|
||||
// Convert description (center label) to 50% position
|
||||
if (connector.description) {
|
||||
labels.push({
|
||||
id: generateId(),
|
||||
text: connector.description,
|
||||
position: 50,
|
||||
height: connector.centerLabelHeight,
|
||||
line: '1'
|
||||
});
|
||||
}
|
||||
|
||||
// Convert endLabel to 90% position
|
||||
if (connector.endLabel) {
|
||||
labels.push({
|
||||
id: generateId(),
|
||||
text: connector.endLabel,
|
||||
position: 90,
|
||||
height: connector.endLabelHeight,
|
||||
line: '1'
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all labels for a connector, migrating legacy labels if needed
|
||||
*/
|
||||
export const getConnectorLabels = (connector: Connector): ConnectorLabel[] => {
|
||||
// If connector already has new-style labels, use them
|
||||
if (connector.labels && connector.labels.length > 0) {
|
||||
return connector.labels;
|
||||
}
|
||||
|
||||
// Otherwise, migrate legacy labels
|
||||
return migrateLegacyLabels(connector);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the actual tile position along the connector path for a given percentage
|
||||
*/
|
||||
export const getLabelTileIndex = (
|
||||
pathLength: number,
|
||||
position: number
|
||||
): number => {
|
||||
if (pathLength === 0) return 0;
|
||||
|
||||
const index = Math.round((position / 100) * (pathLength - 1));
|
||||
return Math.max(0, Math.min(index, pathLength - 1));
|
||||
};
|
||||
@@ -7,3 +7,4 @@ export * from './exportOptions';
|
||||
export * from './model';
|
||||
export * from './findNearestUnoccupiedTile';
|
||||
export * from './pointInPolygon';
|
||||
export * from './connectorLabels';
|
||||
|
||||
Reference in New Issue
Block a user