phase 6
This commit is contained in:
179
frontend/src/lib/canvas/SelectionBox.svelte
Normal file
179
frontend/src/lib/canvas/SelectionBox.svelte
Normal file
@@ -0,0 +1,179 @@
|
||||
<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 -->
|
||||
Reference in New Issue
Block a user