diff --git a/apps/web/src/lib/canvas/Canvas.svelte b/apps/web/src/lib/canvas/Canvas.svelte
index 264e5fd..fd167e8 100644
--- a/apps/web/src/lib/canvas/Canvas.svelte
+++ b/apps/web/src/lib/canvas/Canvas.svelte
@@ -19,6 +19,7 @@
let platform = $derived(c.platform());
let textEditorCurrent = $derived(c.textEditor.current);
let persistenceStatusStore = $derived(c.persistenceStatusStore());
+ let marqueeRect = $derived(c.marqueeRect());
$effect(() => {
c.setCanvasRef(canvasEl);
@@ -70,6 +71,12 @@
spellcheck="false">
{/if}
{/if}
+ {#if marqueeRect}
+
+
+ {/if}
diff --git a/apps/web/src/lib/canvas/canvas-store.svelte.ts b/apps/web/src/lib/canvas/canvas-store.svelte.ts
index e7bb202..aef6ef3 100644
--- a/apps/web/src/lib/canvas/canvas-store.svelte.ts
+++ b/apps/web/src/lib/canvas/canvas-store.svelte.ts
@@ -26,7 +26,7 @@ import {
Store,
TextTool,
} from "inkfinite-core";
-import type { Action, LoadedDoc, PersistenceSink, PersistentDocRepo, Viewport } from "inkfinite-core";
+import type { Action, Box2, LoadedDoc, PersistenceSink, PersistentDocRepo, Viewport } from "inkfinite-core";
import { createRenderer, type Renderer } from "inkfinite-renderer";
import { onDestroy, onMount } from "svelte";
import { SvelteSet } from "svelte/reactivity";
@@ -58,6 +58,14 @@ export function createCanvasController(bindings: CanvasControllerBindings) {
let activeBoardId: string | null = null;
let desktopRepo: DesktopDocRepo | null = null;
let removeBeforeUnload: (() => void) | null = null;
+ const handleResize = () => {
+ if (marqueeBounds) {
+ updateMarquee(marqueeBounds);
+ }
+ };
+ if (typeof window !== "undefined") {
+ window.addEventListener("resize", handleResize);
+ }
let webDb: InkfiniteDB | null = null;
let canvas = $state(null);
@@ -78,6 +86,26 @@ export function createCanvasController(bindings: CanvasControllerBindings) {
const cursorStore = new CursorStore();
const snapStore: SnapStore = createSnapStore();
const brushStore: BrushStore = createBrushStore();
+ type ScreenRect = { left: number; top: number; width: number; height: number };
+ let marqueeBounds: Box2 | null = null;
+ let marqueeRect = $state(null);
+
+ function updateMarquee(bounds: Box2 | null, cameraOverride?: Camera) {
+ marqueeBounds = bounds ? { min: { ...bounds.min }, max: { ...bounds.max } } : null;
+ if (!marqueeBounds) {
+ marqueeRect = null;
+ return;
+ }
+ const viewport = getViewport();
+ const cameraState = cameraOverride ?? store.getState().camera;
+ const minScreen = Camera.worldToScreen(cameraState, marqueeBounds.min, viewport);
+ const maxScreen = Camera.worldToScreen(cameraState, marqueeBounds.max, viewport);
+ const left = Math.min(minScreen.x, maxScreen.x);
+ const top = Math.min(minScreen.y, maxScreen.y);
+ const width = Math.abs(maxScreen.x - minScreen.x);
+ const height = Math.abs(maxScreen.y - minScreen.y);
+ marqueeRect = { left, top, width, height };
+ }
function getViewport(): Viewport {
if (canvas) {
@@ -117,7 +145,10 @@ export function createCanvasController(bindings: CanvasControllerBindings) {
}));
}
- const selectTool = new SelectTool();
+ const handleMarqueeChange = (bounds: Box2 | null) => {
+ updateMarquee(bounds);
+ };
+ const selectTool = new SelectTool(handleMarqueeChange);
const rectTool = new RectTool();
const ellipseTool = new EllipseTool();
const lineTool = new LineTool();
@@ -136,6 +167,11 @@ export function createCanvasController(bindings: CanvasControllerBindings) {
const textEditor = new TextEditorController(store, getViewport, refreshCursor);
const toolController = new ToolController(store, tools);
+ const unsubscribeMarqueeCamera = store.subscribe((state) => {
+ if (marqueeBounds) {
+ updateMarquee(marqueeBounds, state.camera);
+ }
+ });
const history = new HistoryController(bindings);
const desktop = new DesktopFileController(() => repo, () => desktopRepo, (boardId, doc) => {
setActiveBoardId(boardId);
@@ -521,7 +557,11 @@ export function createCanvasController(bindings: CanvasControllerBindings) {
renderer?.dispose();
inputAdapter?.dispose();
persistenceManager?.dispose();
+ unsubscribeMarqueeCamera();
removeBeforeUnload?.();
+ if (typeof window !== "undefined") {
+ window.removeEventListener("resize", handleResize);
+ }
fallbackStatusStore.update(() => ({ backend: "indexeddb", state: "saved", pendingWrites: 0 }));
persistenceStatusStore = fallbackStatusStore;
});
@@ -542,5 +582,6 @@ export function createCanvasController(bindings: CanvasControllerBindings) {
snapStore,
brushStore,
setCanvasRef,
+ marqueeRect: () => marqueeRect,
};
}
diff --git a/packages/core/src/tools/select.ts b/packages/core/src/tools/select.ts
index f903e56..2a84004 100644
--- a/packages/core/src/tools/select.ts
+++ b/packages/core/src/tools/select.ts
@@ -55,8 +55,10 @@ const MIN_RESIZE_SIZE = 5;
export class SelectTool implements Tool {
readonly id: ToolId = "select";
private toolState: SelectToolState;
+ private readonly marqueeListener?: (bounds: Box2 | null) => void;
- constructor() {
+ constructor(onMarqueeChange?: (bounds: Box2 | null) => void) {
+ this.marqueeListener = onMarqueeChange;
this.toolState = {
isDragging: false,
dragStartWorld: null,
@@ -198,6 +200,7 @@ export class SelectTool implements Tool {
if (!isShiftHeld) {
this.toolState.marqueeStart = action.world;
this.toolState.marqueeEnd = action.world;
+ this.notifyMarqueeChange();
return { ...state, ui: { ...state.ui, selectionIds: [] } };
}
@@ -283,6 +286,7 @@ export class SelectTool implements Tool {
if (action.type !== "pointer-move") return state;
this.toolState.marqueeEnd = action.world;
+ this.notifyMarqueeChange();
return state;
}
@@ -310,6 +314,7 @@ export class SelectTool implements Tool {
this.toolState.initialShapePositions.clear();
this.toolState.marqueeStart = null;
this.toolState.marqueeEnd = null;
+ this.notifyMarqueeChange();
return newState;
}
@@ -410,6 +415,7 @@ export class SelectTool implements Tool {
rotationCenter: null,
rotationStartAngle: null,
};
+ this.notifyMarqueeChange();
}
/**
@@ -420,6 +426,12 @@ export class SelectTool implements Tool {
return Box2.fromPoints([this.toolState.marqueeStart, this.toolState.marqueeEnd]);
}
+ private notifyMarqueeChange(): void {
+ if (this.marqueeListener) {
+ this.marqueeListener(this.getMarqueeBounds());
+ }
+ }
+
getHandleAtPoint(state: EditorState, point: Vec2): HandleKind | null {
const hit = this.hitTestHandle(state, point);
return hit?.handle ?? null;