Shared Types — @edv4h/usketch-shared
Key type reference for the shared package.
The @edv4h/usketch-shared package exports all type definitions used across uSketch.
Geometry
interface Point {
x: number;
y: number;
}
interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}
interface Viewport {
x: number;
y: number;
zoom: number;
}
Shape
interface ShapeStyle {
fill: string;
stroke: string;
strokeWidth: number;
opacity: number;
}
interface ShapeData {
id: string;
type: string;
x: number;
y: number;
width: number;
height: number;
style: ShapeStyle;
rotation?: number;
[key: string]: unknown;
}
type ResizeHandle = "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w";
const DEFAULT_STYLE: ShapeStyle = {
fill: "#ffffff",
stroke: "#1e1e1e",
strokeWidth: 2,
opacity: 1,
};
Plugin
/** Cleanup function returned from setup. May be sync or async. */
type PluginTeardown = () => void | Promise<void>;
interface UsketchPlugin {
readonly id: string;
readonly name: string;
// Return a teardown function from setup if the plugin needs cleanup; return
// void otherwise. The old `teardown` property was removed — stashing cleanup
// on `this` is unsafe under React StrictMode (a second setup call overwrites it).
setup(ctx: PluginContext): PluginTeardown | void | Promise<PluginTeardown | void>;
}
interface PluginContext {
store: BoardStore;
layers: LayerManager;
tools: ToolRegistry;
shapes: ShapeRegistry;
commands: CommandRegistry;
shortcuts: ShortcutRegistry;
events: EventBus;
transient: TransientRegistry;
lod: LodController;
ui: UiRegistry;
externalContent: ExternalContentRegistry;
actions: ActionRegistry;
services: ServiceRegistry;
}
Shape System
interface ShapeDefinition {
render: (data: ShapeData) => ReactElement;
getBounds: (data: ShapeData) => BoundingBox;
hitTest: (data: ShapeData, point: Point) => boolean;
resize: (data: ShapeData, handle: ResizeHandle, delta: Point) => ShapeData;
createDefault: (params: { id: string; x: number; y: number }) => ShapeData;
renderTarget?: "svg" | "html";
minSize?: { width: number; height: number };
// Whether the user can resize the shape. Default true. A predicate allows
// per-instance control.
resizable?: boolean | ((data: ShapeData) => boolean);
// Container behavior for shapes with children (shapes referencing this via
// parentId). Omit for non-container shapes.
container?: {
enabled?: boolean | ((data: ShapeData) => boolean);
selectableChildren?: boolean | ((data: ShapeData) => boolean);
autoAttach?: boolean | ((data: ShapeData) => boolean);
layout?: (ctx: { container: ShapeData; children: ShapeData[] }) =>
Array<{ id: string; patch: Partial<ShapeData> }>;
};
// Child-side "attachable" behavior — the counterpart to `container`. Sticks
// to and follows any shape it is dropped on.
attachable?: {
toAny?: boolean | ((target: ShapeData) => boolean);
follow?: boolean | ((data: ShapeData) => boolean);
hitTest?: "center" | "contain";
};
move?: (data: ShapeData, dx: number, dy: number) => Partial<ShapeData>;
applyBounds?: (data: ShapeData, newBounds: BoundingBox) => Partial<ShapeData>;
// Return GPU-renderable primitive data, or null to fall back to DOM rendering.
gpuPrimitive?: (data: ShapeData) => GpuPrimitive | null;
// Lightweight component used in LOD (zoomed-out) mode. Falls back to a
// solid-fill rectangle when omitted.
simplifiedComponent?: ComponentType<{ shape: ShapeData }>;
// Project this shape into a flat record for AI prompt embedding.
serializeForAi?: (data: ShapeData, ctx?: ShapeSerializeContext) => Record<string, unknown>;
// Project into a recognition-friendly form (OCR / handwriting). null if N/A.
serializeForRecognition?: (data: ShapeData, ctx?: ShapeSerializeContext) => unknown;
// Project into a key/value map for the debug HUD shapes panel.
debugFields?: (data: ShapeData) => Record<string, unknown>;
}
interface ShapeRegistry {
register(type: string, definition: ShapeDefinition): void;
get(type: string): ShapeDefinition | undefined;
getAll(): ReadonlyMap<string, ShapeDefinition>;
}
Tool System
interface ToolDefinition {
icon: () => ReactElement;
cursor?: string;
shortcut?: string;
order?: number;
onActivate?: (ctx: ToolContext) => void;
onDeactivate?: (ctx: ToolContext) => void;
onPointerDown?: (ctx: ToolContext, event: CanvasPointerEvent) => void;
onPointerMove?: (ctx: ToolContext, event: CanvasPointerEvent) => void;
onPointerUp?: (ctx: ToolContext, event: CanvasPointerEvent) => void;
}
interface ToolContext {
store: BoardStore;
shapes: ShapeRegistry;
commands: CommandRegistry;
events: EventBus;
}
interface CanvasPointerEvent {
worldPoint: Point;
screenPoint: Point;
shiftKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
altKey: boolean;
button: number;
}
interface CanvasWheelEvent {
screenPoint: Point;
worldPoint: Point;
deltaX: number;
deltaY: number;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
}
interface ToolRegistry {
register(id: string, definition: ToolDefinition): void;
get(id: string): ToolDefinition | undefined;
getAll(): ReadonlyMap<string, ToolDefinition>;
getOrdered(): readonly { id: string; definition: ToolDefinition }[];
}
Layer System
interface Layer {
id: string;
order: number;
render: (ctx: LayerRenderContext) => ReactElement | null;
interactable?: boolean;
fixed?: boolean;
}
interface LayerRenderContext {
viewport: Viewport;
shapes: ReadonlyMap<string, ShapeData>;
// Shapes sorted by zIndex ascending (back to front). Reflects any active filter.
shapesSorted: readonly ShapeData[];
selection: ReadonlySet<string>;
// Id of the shape currently hovered, or null. The hover counterpart to selection.
hoveredShapeId: string | null;
theme: Theme;
// Current LOD render mode. Layers should adapt their output accordingly.
renderMode: RenderMode;
// Visible region in world coords (from viewport + canvas size). Enables
// per-shape viewport decisions (off-screen LOD, culling). width/height are 0
// until the canvas is measured.
viewportBounds: BoundingBox;
}
interface LayerManager {
register(layer: Layer): void;
unregister(layerId: string): void;
getLayers(): readonly Layer[];
}
Command System
interface Command {
execute(): void;
undo(): void;
}
interface CommandRegistry {
execute(command: Command): void;
undo(): void;
redo(): void;
canUndo(): boolean;
canRedo(): boolean;
getHistorySize(): number;
getCursor(): number;
}
Shortcut System
interface ShortcutRegistry {
register(combo: string, callback: () => void): () => void;
handleKeyDown(event: KeyboardEvent): boolean;
}
Event Bus
Known core events (CoreEventMap) get typed on / emit overloads; plugins can still freely emit/on their own events via the string fallback.
interface EventBus {
// Known CoreEventMap events are handled by typed overloads
on<K extends keyof CoreEventMap>(event: K, handler: (data: CoreEventMap[K]) => void): () => void;
on<T = unknown>(event: string, handler: (data: T) => void): () => void;
emit<K extends keyof CoreEventMap>(event: K, data: CoreEventMap[K]): void;
emit<T = unknown>(event: string, data: T): void;
// Suspend event delivery. Ref-counted — each pause() must be paired with one resume().
pause(): void;
resume(): void;
isPaused(): boolean;
}
Transient System
interface TransientObject {
id: string;
type: string;
sourceUserId: string;
position: Point;
data: Record<string, unknown>;
ttl?: number;
createdAt: number;
}
interface TransientRenderer {
render: (obj: TransientObject, ctx: LayerRenderContext) => ReactElement;
}
interface TransientRegistry {
registerType(type: string, renderer: TransientRenderer): void;
getRenderer(type: string): TransientRenderer | undefined;
emit(obj: TransientObject): void;
dismiss(id: string): void;
getAll(): ReadonlyMap<string, TransientObject>;
subscribe(listener: () => void): () => void;
}
Board Store
interface BoardStore {
// Shape CRUD
getShapes(): ReadonlyMap<string, ShapeData>;
// Shapes sorted by zIndex (ascending = back to front). Cached internally.
getShapesSorted(): readonly ShapeData[];
getShape(id: string): ShapeData | undefined;
addShape(shape: ShapeData): void;
// id is fixed by the first argument and cannot be changed via updates.
updateShape(id: string, updates: Partial<Omit<ShapeData, "id">>): void;
deleteShape(id: string): void;
// Assign zIndex to any shapes that don't have one (used after bulk load).
ensureZIndex(): void;
// Selection
getSelection(): ReadonlySet<string>;
setSelection(ids: string[]): void;
addToSelection(id: string): void;
removeFromSelection(id: string): void;
clearSelection(): void;
// Hover (a UI signal set by the active tool)
getHoveredShapeId(): string | null;
setHoveredShapeId(id: string | null): void;
// Active tool / default tool
getActiveToolId(): string;
setActiveToolId(id: string): void;
getDefaultToolId(): string;
setDefaultToolId(id: string): void;
resetToDefaultTool(): void;
// Viewport
getViewport(): Viewport;
setViewport(viewport: Viewport): void;
panBy(dx: number, dy: number): void;
zoomTo(zoom: number, center: Point): void;
// Center the viewport so bounds fits within viewportSize, leaving padding px per side.
fitToBounds(
bounds: BoundingBox,
viewportSize: { width: number; height: number },
padding?: number,
): void;
// Style
getStyleSettings(): ShapeStyle;
setStyleSettings(style: Partial<ShapeStyle>): void;
// Visibility (IDs whose bounds intersect the given world-space viewport)
getVisibleShapeIds(viewportBounds: BoundingBox): string[];
// Subscriptions
subscribe(listener: () => void): () => void;
onMutation(listener: (event: StoreEvent) => void): () => void;
}
// StoreEvent is a closed discriminated union — narrowing on `type` types `payload`
// correctly. Shape-related payloads are normalized to `ids: string[]` (length 1 even
// for single changes); a legacy `id` is also carried for backward compatibility.
type StoreEvent =
| { type: "shape:added"; payload: { id: string; ids: string[] } }
| { type: "shape:removed"; payload: { id: string; ids: string[] } }
| { type: "shape:updated"; payload: ShapeChange & { ids: string[] } }
| { type: "selection:changed"; payload?: { ids: string[] } }
| { type: "tool:changed"; payload: { id: string } }
| { type: "default-tool:changed"; payload: { id: string } }
| { type: "shapes:z-index-initialized"; payload: { count: number } }
| { type: "viewport:changed"; payload?: undefined }
| { type: "style:changed"; payload?: undefined };