Something went wrong. Try again.
inkfinite (an infinite canvas/whiteboard) monorepo ink.stormlightlabs.org
svelte rust tauri svg canvas perfect-freehand
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124import type { CursorState } from '@inkfinite/core';import { shapeBoundsForState } from '@inkfinite/core/geometry';import { type Box2, Box2 as Box2Ops, type Vec2, Vec2 as Vec2Ops } from '@inkfinite/core/geometry';import type { EditorState, ToolId } from '@inkfinite/core';import { getSelectedShapes } from '@inkfinite/core';
export type SelectionSummary = { count: number; kind?: string; bounds?: { w: number; h: number } };
export type SnapSummary = { enabled: boolean; gridSize?: number; angleStepDeg?: number };
export type PersistenceStatus = { backend: 'indexeddb' | 'filesystem'; state: 'saved' | 'saving' | 'error'; lastSavedAt?: number; pendingWrites?: number; errorMsg?: string;};
export type StatusBarVM = { cursorWorld: Vec2; cursorScreen?: Vec2; toolId: ToolId; mode: 'idle' | 'dragging' | 'panning' | 'text-edit' | string; selection: SelectionSummary; snap: SnapSummary; persistence: PersistenceStatus;};
/** * Convert the current camera zoom factor into a human-friendly percentage. */export function getZoomPct(state: EditorState): number { const pct = state.camera.zoom * 100; if (!Number.isFinite(pct)) { return 100; } return Math.round(pct);}
/** * Get the active tool identifier from UI state. */export function getToolId(state: EditorState): ToolId { return state.ui.toolId;}
/** * Summarize the current selection for display. */export function getSelectionSummary(state: EditorState): SelectionSummary { const shapes = getSelectedShapes(state); const count = shapes.length;
if (count === 0) { return { count: 0 }; }
const combinedBounds = combineBounds(shapes.map((shape) => shapeBoundsForState(state, shape)));
const kind = count === 1 ? shapes[0].type : shapes.every((shape) => shape.type === shapes[0].type) ? shapes[0].type : 'mixed';
return { count, kind, bounds: combinedBounds ? { w: Box2Ops.width(combinedBounds), h: Box2Ops.height(combinedBounds) } : undefined };}
const SNAP_DEFAULT: SnapSummary = { enabled: false };
/** * Provide safe defaults for snap/grid summary until features are enabled. */export function getSnapSummary(_: EditorState): SnapSummary { return { ...SNAP_DEFAULT };}
/** * Compose the full StatusBar view model from editor/cursor/persistence state. */export function buildStatusBarVM( editorState: EditorState, cursorState: CursorState, persistence: PersistenceStatus, mode: StatusBarVM['mode'] = 'idle'): StatusBarVM { return { cursorWorld: Vec2Ops.clone(cursorState.cursorWorld), cursorScreen: cursorState.cursorScreen ? Vec2Ops.clone(cursorState.cursorScreen) : undefined, toolId: getToolId(editorState), mode, selection: getSelectionSummary(editorState), snap: getSnapSummary(editorState), persistence: { ...persistence } };}
function combineBounds(boxes: Box2[]): Box2 | null { if (boxes.length === 0) { return null; } let combined = Box2Ops.clone(boxes[0]); for (let index = 1; index < boxes.length; index++) { const box = boxes[index]; combined = { min: { x: Math.min(combined.min.x, box.min.x), y: Math.min(combined.min.y, box.min.y) }, max: { x: Math.max(combined.max.x, box.max.x), y: Math.max(combined.max.y, box.max.y) } }; } return combined;}