|
107 | 107 | import ZoomRange from '$components/base/input/ZoomRange.svelte'; |
108 | 108 | import DesignerCrosshair from '$components/DesignerCrosshair.svelte'; |
109 | 109 | import DesignerGrid from '$components/DesignerGrid.svelte'; |
110 | | - import { FINE_MOVEMENT_DELTA, FINE_MOVEMENT_SHIFT_MULTIPLIER } from '$lib/constants'; |
| 110 | + import { type AlignmentLine, detectAlignments } from '$lib/alignment'; |
| 111 | + import { |
| 112 | + ALIGNMENT_LINE_COLOR, |
| 113 | + ALIGNMENT_LINE_WIDTH, |
| 114 | + FINE_MOVEMENT_DELTA, |
| 115 | + FINE_MOVEMENT_SHIFT_MULTIPLIER |
| 116 | + } from '$lib/constants'; |
111 | 117 | import { |
112 | 118 | getContextMenuItemForCircle, |
113 | 119 | modifyCircle, |
|
277 | 283 | /** Hover info tooltip showing element details and keyboard shortcuts */ |
278 | 284 | let hoverInfo: HoverInfo | undefined = $state(); |
279 | 285 |
|
| 286 | + /** Alignment lines to display during element dragging */ |
| 287 | + let alignmentLines: AlignmentLine[] = $state([]); |
| 288 | +
|
| 289 | + /** |
| 290 | + * Detects and updates alignment lines for the currently dragged element |
| 291 | + * |
| 292 | + * Called during ondragmove events to show visual alignment guides. |
| 293 | + * Uses the live position from the drag event (not the stored position) |
| 294 | + * to provide real-time alignment feedback. |
| 295 | + * |
| 296 | + * @param event - Konva drag event with current position |
| 297 | + * @param draggedElement - The element being dragged (for ID and properties) |
| 298 | + */ |
| 299 | + const updateAlignmentLines = ( |
| 300 | + event: KonvaDragTransformEvent, |
| 301 | + draggedElement: CircleData | RectangleData | LegData |
| 302 | + ) => { |
| 303 | + // Create a copy of the element with the current drag position |
| 304 | + const liveElement = { |
| 305 | + ...draggedElement, |
| 306 | + x: event.target.x(), |
| 307 | + y: event.target.y() |
| 308 | + }; |
| 309 | +
|
| 310 | + // Combine all elements except the dragged one |
| 311 | + const allOtherElements = [ |
| 312 | + ...$projectStore.circles.filter((c) => c.id !== draggedElement.id), |
| 313 | + ...$projectStore.rectangles.filter((r) => r.id !== draggedElement.id), |
| 314 | + ...$projectStore.legs.filter((l) => l.id !== draggedElement.id) |
| 315 | + ]; |
| 316 | +
|
| 317 | + alignmentLines = detectAlignments( |
| 318 | + liveElement, |
| 319 | + allOtherElements, |
| 320 | + $projectStore.panelSettings.width, |
| 321 | + $projectStore.panelSettings.height |
| 322 | + ); |
| 323 | + }; |
| 324 | +
|
| 325 | + /** |
| 326 | + * Clears all alignment lines |
| 327 | + * |
| 328 | + * Called on dragend events to remove visual guides after dragging completes. |
| 329 | + */ |
| 330 | + const clearAlignmentLines = () => { |
| 331 | + alignmentLines = []; |
| 332 | + }; |
| 333 | +
|
280 | 334 | /** |
281 | 335 | * Mouse enter event handler - shows hover info and enables fine movement |
282 | 336 | * |
|
439 | 493 | ondblclick={() => { |
440 | 494 | if (mode === 'pointer') modifyCircle(circle); |
441 | 495 | }} |
442 | | - ondragend={() => updateCircleChanges()} |
443 | | - ondragmove={(event) => limitCircle(event, circle)} |
| 496 | + ondragend={() => { |
| 497 | + updateCircleChanges(); |
| 498 | + clearAlignmentLines(); |
| 499 | + }} |
| 500 | + ondragmove={(event) => { |
| 501 | + limitCircle(event, circle); |
| 502 | + updateAlignmentLines(event, circle); |
| 503 | + }} |
444 | 504 | onmouseenter={(event) => handleMouseEnter(event, circle)} |
445 | 505 | onmouseleave={(event) => handleMouseLeave(event, circle)} |
446 | 506 | opacity={0.75} |
|
460 | 520 | ondblclick={() => { |
461 | 521 | if (mode === 'pointer') modifyRectangle(rectangle); |
462 | 522 | }} |
463 | | - ondragend={() => updateRectangleChanges()} |
464 | | - ondragmove={(event) => limitBox(event, rectangle)} |
| 523 | + ondragend={() => { |
| 524 | + updateRectangleChanges(); |
| 525 | + clearAlignmentLines(); |
| 526 | + }} |
| 527 | + ondragmove={(event) => { |
| 528 | + limitBox(event, rectangle); |
| 529 | + updateAlignmentLines(event, rectangle); |
| 530 | + }} |
465 | 531 | onmouseenter={(event) => handleMouseEnter(event, rectangle)} |
466 | 532 | onmouseleave={(event) => handleMouseLeave(event, rectangle)} |
467 | 533 | opacity={0.75} |
|
480 | 546 | ondblclick={() => { |
481 | 547 | if (mode === 'pointer') deleteLegWithConfirm(leg); |
482 | 548 | }} |
483 | | - ondragend={() => updateLegChanges()} |
484 | | - ondragmove={(event) => limitBox(event, leg)} |
| 549 | + ondragend={() => { |
| 550 | + updateLegChanges(); |
| 551 | + clearAlignmentLines(); |
| 552 | + }} |
| 553 | + ondragmove={(event) => { |
| 554 | + limitBox(event, leg); |
| 555 | + updateAlignmentLines(event, leg); |
| 556 | + }} |
485 | 557 | onmouseenter={(event) => handleMouseEnter(event, leg)} |
486 | 558 | onmouseleave={(event) => handleMouseLeave(event, leg)} |
487 | 559 | opacity={0.75} |
|
490 | 562 | bind:y={leg.y} |
491 | 563 | /> |
492 | 564 | {/each} |
| 565 | + {#if mode === 'pointer'} |
| 566 | + {#each alignmentLines as line} |
| 567 | + <Line |
| 568 | + listening={false} |
| 569 | + opacity={0.9} |
| 570 | + points={[line.x1, line.y1, line.x2, line.y2]} |
| 571 | + stroke={ALIGNMENT_LINE_COLOR} |
| 572 | + strokeWidth={ALIGNMENT_LINE_WIDTH} |
| 573 | + /> |
| 574 | + {/each} |
| 575 | + {/if} |
493 | 576 | {#if mode === 'measure'} |
494 | 577 | <DesignerGrid |
495 | 578 | height={$projectStore.panelSettings.height} |
|
0 commit comments