web based infinite canvas
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: marquee!

+72 -3
+16
apps/web/src/lib/canvas/Canvas.svelte
··· 19 19 let platform = $derived(c.platform()); 20 20 let textEditorCurrent = $derived(c.textEditor.current); 21 21 let persistenceStatusStore = $derived(c.persistenceStatusStore()); 22 + let marqueeRect = $derived(c.marqueeRect()); 22 23 23 24 $effect(() => { 24 25 c.setCanvasRef(canvasEl); ··· 70 71 spellcheck="false"></textarea> 71 72 {/if} 72 73 {/if} 74 + {#if marqueeRect} 75 + <div 76 + class="canvas-marquee" 77 + style={`left:${marqueeRect.left}px;top:${marqueeRect.top}px;width:${marqueeRect.width}px;height:${marqueeRect.height}px;`}> 78 + </div> 79 + {/if} 73 80 </div> 74 81 <HistoryViewer store={c.store} bind:open={historyViewerOpen} onClose={c.history.handleClose} /> 75 82 <StatusBar ··· 126 133 box-shadow: 127 134 0 0 0 1px rgba(0, 0, 0, 0.05), 128 135 0 8px 20px rgba(0, 0, 0, 0.15); 136 + } 137 + 138 + .canvas-marquee { 139 + position: absolute; 140 + border: 1px solid rgba(136, 192, 208, 0.7); 141 + background-color: rgba(136, 192, 208, 0.2); 142 + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2) inset; 143 + pointer-events: none; 144 + z-index: 1; 129 145 } 130 146 </style>
+43 -2
apps/web/src/lib/canvas/canvas-store.svelte.ts
··· 26 26 Store, 27 27 TextTool, 28 28 } from "inkfinite-core"; 29 - import type { Action, LoadedDoc, PersistenceSink, PersistentDocRepo, Viewport } from "inkfinite-core"; 29 + import type { Action, Box2, LoadedDoc, PersistenceSink, PersistentDocRepo, Viewport } from "inkfinite-core"; 30 30 import { createRenderer, type Renderer } from "inkfinite-renderer"; 31 31 import { onDestroy, onMount } from "svelte"; 32 32 import { SvelteSet } from "svelte/reactivity"; ··· 58 58 let activeBoardId: string | null = null; 59 59 let desktopRepo: DesktopDocRepo | null = null; 60 60 let removeBeforeUnload: (() => void) | null = null; 61 + const handleResize = () => { 62 + if (marqueeBounds) { 63 + updateMarquee(marqueeBounds); 64 + } 65 + }; 66 + if (typeof window !== "undefined") { 67 + window.addEventListener("resize", handleResize); 68 + } 61 69 let webDb: InkfiniteDB | null = null; 62 70 let canvas = $state<HTMLCanvasElement | null>(null); 63 71 ··· 78 86 const cursorStore = new CursorStore(); 79 87 const snapStore: SnapStore = createSnapStore(); 80 88 const brushStore: BrushStore = createBrushStore(); 89 + type ScreenRect = { left: number; top: number; width: number; height: number }; 90 + let marqueeBounds: Box2 | null = null; 91 + let marqueeRect = $state<ScreenRect | null>(null); 92 + 93 + function updateMarquee(bounds: Box2 | null, cameraOverride?: Camera) { 94 + marqueeBounds = bounds ? { min: { ...bounds.min }, max: { ...bounds.max } } : null; 95 + if (!marqueeBounds) { 96 + marqueeRect = null; 97 + return; 98 + } 99 + const viewport = getViewport(); 100 + const cameraState = cameraOverride ?? store.getState().camera; 101 + const minScreen = Camera.worldToScreen(cameraState, marqueeBounds.min, viewport); 102 + const maxScreen = Camera.worldToScreen(cameraState, marqueeBounds.max, viewport); 103 + const left = Math.min(minScreen.x, maxScreen.x); 104 + const top = Math.min(minScreen.y, maxScreen.y); 105 + const width = Math.abs(maxScreen.x - minScreen.x); 106 + const height = Math.abs(maxScreen.y - minScreen.y); 107 + marqueeRect = { left, top, width, height }; 108 + } 81 109 82 110 function getViewport(): Viewport { 83 111 if (canvas) { ··· 117 145 })); 118 146 } 119 147 120 - const selectTool = new SelectTool(); 148 + const handleMarqueeChange = (bounds: Box2 | null) => { 149 + updateMarquee(bounds); 150 + }; 151 + const selectTool = new SelectTool(handleMarqueeChange); 121 152 const rectTool = new RectTool(); 122 153 const ellipseTool = new EllipseTool(); 123 154 const lineTool = new LineTool(); ··· 136 167 137 168 const textEditor = new TextEditorController(store, getViewport, refreshCursor); 138 169 const toolController = new ToolController(store, tools); 170 + const unsubscribeMarqueeCamera = store.subscribe((state) => { 171 + if (marqueeBounds) { 172 + updateMarquee(marqueeBounds, state.camera); 173 + } 174 + }); 139 175 const history = new HistoryController(bindings); 140 176 const desktop = new DesktopFileController(() => repo, () => desktopRepo, (boardId, doc) => { 141 177 setActiveBoardId(boardId); ··· 521 557 renderer?.dispose(); 522 558 inputAdapter?.dispose(); 523 559 persistenceManager?.dispose(); 560 + unsubscribeMarqueeCamera(); 524 561 removeBeforeUnload?.(); 562 + if (typeof window !== "undefined") { 563 + window.removeEventListener("resize", handleResize); 564 + } 525 565 fallbackStatusStore.update(() => ({ backend: "indexeddb", state: "saved", pendingWrites: 0 })); 526 566 persistenceStatusStore = fallbackStatusStore; 527 567 }); ··· 542 582 snapStore, 543 583 brushStore, 544 584 setCanvasRef, 585 + marqueeRect: () => marqueeRect, 545 586 }; 546 587 }
+13 -1
packages/core/src/tools/select.ts
··· 55 55 export class SelectTool implements Tool { 56 56 readonly id: ToolId = "select"; 57 57 private toolState: SelectToolState; 58 + private readonly marqueeListener?: (bounds: Box2 | null) => void; 58 59 59 - constructor() { 60 + constructor(onMarqueeChange?: (bounds: Box2 | null) => void) { 61 + this.marqueeListener = onMarqueeChange; 60 62 this.toolState = { 61 63 isDragging: false, 62 64 dragStartWorld: null, ··· 198 200 if (!isShiftHeld) { 199 201 this.toolState.marqueeStart = action.world; 200 202 this.toolState.marqueeEnd = action.world; 203 + this.notifyMarqueeChange(); 201 204 202 205 return { ...state, ui: { ...state.ui, selectionIds: [] } }; 203 206 } ··· 283 286 if (action.type !== "pointer-move") return state; 284 287 285 288 this.toolState.marqueeEnd = action.world; 289 + this.notifyMarqueeChange(); 286 290 287 291 return state; 288 292 } ··· 310 314 this.toolState.initialShapePositions.clear(); 311 315 this.toolState.marqueeStart = null; 312 316 this.toolState.marqueeEnd = null; 317 + this.notifyMarqueeChange(); 313 318 314 319 return newState; 315 320 } ··· 410 415 rotationCenter: null, 411 416 rotationStartAngle: null, 412 417 }; 418 + this.notifyMarqueeChange(); 413 419 } 414 420 415 421 /** ··· 418 424 getMarqueeBounds(): Box2 | null { 419 425 if (!this.toolState.marqueeStart || !this.toolState.marqueeEnd) return null; 420 426 return Box2.fromPoints([this.toolState.marqueeStart, this.toolState.marqueeEnd]); 427 + } 428 + 429 + private notifyMarqueeChange(): void { 430 + if (this.marqueeListener) { 431 + this.marqueeListener(this.getMarqueeBounds()); 432 + } 421 433 } 422 434 423 435 getHandleAtPoint(state: EditorState, point: Vec2): HandleKind | null {