180 lines
4.4 KiB
Svelte
180 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Selection box visual indicator for canvas
|
|
* Displays a border and resize handles around selected images
|
|
*/
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import Konva from 'konva';
|
|
import { selection, selectionCount } from '$lib/stores/selection';
|
|
|
|
export let layer: Konva.Layer | null = null;
|
|
export let getImageBounds: (
|
|
id: string
|
|
) => { x: number; y: number; width: number; height: number } | null;
|
|
|
|
let selectionGroup: Konva.Group | null = null;
|
|
let unsubscribe: (() => void) | null = null;
|
|
|
|
onMount(() => {
|
|
if (!layer) return;
|
|
|
|
// Create group for selection visuals
|
|
selectionGroup = new Konva.Group({
|
|
listening: false,
|
|
name: 'selection-group',
|
|
});
|
|
|
|
layer.add(selectionGroup);
|
|
|
|
// Subscribe to selection changes
|
|
unsubscribe = selection.subscribe(() => {
|
|
updateSelectionVisuals();
|
|
});
|
|
|
|
layer.batchDraw();
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (unsubscribe) unsubscribe();
|
|
if (selectionGroup) {
|
|
selectionGroup.destroy();
|
|
selectionGroup = null;
|
|
}
|
|
if (layer) layer.batchDraw();
|
|
});
|
|
|
|
/**
|
|
* Update selection visual indicators
|
|
*/
|
|
function updateSelectionVisuals() {
|
|
if (!selectionGroup || !layer) return;
|
|
|
|
// Clear existing visuals
|
|
selectionGroup.destroyChildren();
|
|
|
|
const selectedIds = selection.getSelectedIds();
|
|
if (selectedIds.length === 0) {
|
|
layer.batchDraw();
|
|
return;
|
|
}
|
|
|
|
// Calculate bounding box of all selected images
|
|
let minX = Infinity;
|
|
let minY = Infinity;
|
|
let maxX = -Infinity;
|
|
let maxY = -Infinity;
|
|
|
|
selectedIds.forEach((id) => {
|
|
const bounds = getImageBounds(id);
|
|
if (bounds) {
|
|
minX = Math.min(minX, bounds.x);
|
|
minY = Math.min(minY, bounds.y);
|
|
maxX = Math.max(maxX, bounds.x + bounds.width);
|
|
maxY = Math.max(maxY, bounds.y + bounds.height);
|
|
}
|
|
});
|
|
|
|
if (!isFinite(minX) || !isFinite(minY)) {
|
|
layer.batchDraw();
|
|
return;
|
|
}
|
|
|
|
const width = maxX - minX;
|
|
const height = maxY - minY;
|
|
|
|
// Draw selection border
|
|
const border = new Konva.Rect({
|
|
x: minX,
|
|
y: minY,
|
|
width,
|
|
height,
|
|
stroke: '#3b82f6',
|
|
strokeWidth: 2,
|
|
dash: [8, 4],
|
|
listening: false,
|
|
});
|
|
|
|
selectionGroup.add(border);
|
|
|
|
// Draw resize handles if single selection
|
|
if ($selectionCount === 1) {
|
|
const handleSize = 8;
|
|
const handlePositions = [
|
|
{ x: minX, y: minY, cursor: 'nw-resize' }, // Top-left
|
|
{ x: minX + width / 2, y: minY, cursor: 'n-resize' }, // Top-center
|
|
{ x: maxX, y: minY, cursor: 'ne-resize' }, // Top-right
|
|
{ x: maxX, y: minY + height / 2, cursor: 'e-resize' }, // Right-center
|
|
{ x: maxX, y: maxY, cursor: 'se-resize' }, // Bottom-right
|
|
{ x: minX + width / 2, y: maxY, cursor: 's-resize' }, // Bottom-center
|
|
{ x: minX, y: maxY, cursor: 'sw-resize' }, // Bottom-left
|
|
{ x: minX, y: minY + height / 2, cursor: 'w-resize' }, // Left-center
|
|
];
|
|
|
|
handlePositions.forEach((pos) => {
|
|
const handle = new Konva.Rect({
|
|
x: pos.x - handleSize / 2,
|
|
y: pos.y - handleSize / 2,
|
|
width: handleSize,
|
|
height: handleSize,
|
|
fill: '#3b82f6',
|
|
stroke: '#ffffff',
|
|
strokeWidth: 1,
|
|
listening: false,
|
|
});
|
|
|
|
selectionGroup!.add(handle);
|
|
});
|
|
}
|
|
|
|
// Draw selection count badge if multiple selection
|
|
if ($selectionCount > 1) {
|
|
const badgeX = maxX - 30;
|
|
const badgeY = minY - 30;
|
|
|
|
const badge = new Konva.Group({
|
|
x: badgeX,
|
|
y: badgeY,
|
|
listening: false,
|
|
});
|
|
|
|
const badgeBackground = new Konva.Rect({
|
|
x: 0,
|
|
y: 0,
|
|
width: 30,
|
|
height: 24,
|
|
fill: '#3b82f6',
|
|
cornerRadius: 4,
|
|
listening: false,
|
|
});
|
|
|
|
const badgeText = new Konva.Text({
|
|
x: 0,
|
|
y: 0,
|
|
width: 30,
|
|
height: 24,
|
|
text: $selectionCount.toString(),
|
|
fontSize: 14,
|
|
fill: '#ffffff',
|
|
align: 'center',
|
|
verticalAlign: 'middle',
|
|
listening: false,
|
|
});
|
|
|
|
badge.add(badgeBackground);
|
|
badge.add(badgeText);
|
|
selectionGroup!.add(badge);
|
|
}
|
|
|
|
layer.batchDraw();
|
|
}
|
|
|
|
/**
|
|
* Force update of selection visuals (for external calls)
|
|
*/
|
|
export function update() {
|
|
updateSelectionVisuals();
|
|
}
|
|
</script>
|
|
|
|
<!-- This component doesn't render any DOM, it only manages Konva nodes -->
|