Commit Graph
56 Commits
Author SHA1 Message Date
Hiren Kangad e3c3aaf558 fix(refboard): server posters respect culling budget + fix temp dir leak
1. Server posters no longer auto-load in VideoSprite constructor.
   loadServerPoster() is now public and called by the culling system
   only when within MAX_POSTER_VIDEOS budget. Videos outside the
   budget stay as placeholders regardless of server poster availability.

2. video-utils cleanup() now uses statSync to distinguish files from
   directories, unlinks files first, then rmdirs. Previously tmpDir
   was passed through path.dirname() which resolved to /tmp instead
   of the created temp directory.
2026-03-10 12:19:43 +05:30
Hiren Kangad 198497381f feat(refboard): server-generated video posters via ffmpeg
Upload pipeline now extracts poster frame (JPEG) and metadata
(width, height, duration) from videos at upload time using ffmpeg.

Server:
- Add ffmpeg to Docker image (Alpine)
- video-utils.js: probeVideo() and extractPoster() using ffprobe/ffmpeg
- Upload response includes poster_asset_key and duration

Frontend:
- VideoObject gains poster and duration fields in scene format
- VideoSprite accepts posterAssetKey + TextureManager, loads poster
  as a regular image texture on construction (no <video> needed)
- Server poster loaded/released via TextureManager ref counting
- addVideoFromUpload passes poster and duration through to scene data
- image-drop.ts forwards poster_asset_key from upload response

Result: videos with server posters render as images by default.
Zero <video> elements needed for thumbnails. Only explicit play
creates a media element.
2026-03-10 12:10:18 +05:30
Hiren Kangad 9ecb287976 fix(refboard): force seek to decode poster frame with preload='metadata'
preload='metadata' doesn't decode video frames, so readyState stays
at 1 (HAVE_METADATA) and loadeddata never fires. capturePoster() now
detects this state and forces a seek to 0.1s via _trySeekCapture(),
which triggers frame decode and captures on the 'seeked' event.
2026-03-10 12:02:47 +05:30
Hiren Kangad 89010335be feat(refboard): video budget system, lazy lifecycle, incremental sync fixes
Video memory:
- Lazy <video> creation: constructor makes placeholder only, initVideo()
  creates element with preload='metadata' when near viewport
- Tiered budgets: 1 playing / 6 initialized / 4 poster textures max
- Aggressive teardown: offscreen videos lose <video> element, poster,
  and all textures — zero memory for offscreen videos
- Poster textures dropped while playing (don't keep both resident)
- Dimension caching in scene data (nativeW/nativeH) avoids re-init

Server:
- HTTP Range support (206 Partial Content) for video seeking
- Proper end clamping and invalid range rejection (416)

Sync:
- Remove redundant broadcastSceneDebounced() from broadcastTransform()
- Remove duplicate broadcastElements() from drag-end handler

VideoControls:
- Event-driven (timeupdate/play/pause/seeked) instead of rAF polling
- Tracks actual HTMLVideoElement reference, rebinds on init/teardown
- Seek uses ref-based commit on pointerUp, not conditional onChange
2026-03-10 11:56:33 +05:30
Hiren Kangad ae050d2119 fix(refboard): restore drag-drop with fallback + React event handlers
- Add onDragOver/onDrop preventDefault on container div (React level)
  to always block browser default file-open behavior
- Add DOM fallback in useCanvasSetup if ref is somehow null
- setupDragDrop's native listeners handle the actual upload logic
2026-03-10 11:22:36 +05:30
Hiren Kangad 205ce65a8b fix(refboard): drag-drop opening in new tab — use ref for drop target
The drag/drop setup was searching for a <canvas> element to find its
parent, which was fragile and could fail (returning null). When it
failed, document-level dragover/drop prevention was never registered,
so the browser's default behavior (open file in new tab) kicked in.

Fix: pass a direct ref to the outer canvas container div from Editor
to useCanvasSetup. No more DOM querying for the drop target.
2026-03-10 11:20:26 +05:30
Hiren Kangad 9716beee8a fix(refboard): reference-counted textures prevent shared source crash
When duplicated images share the same assetKey, Assets.unload()
was destroying the shared texture source — crashing all sprites
using it (null addressModeU/alphaMode in WebGL renderer).

TextureManager now tracks refCount per asset. Only calls
Assets.unload() when the last reference is released. ImageSprite
calls release() instead of unload() on texture teardown.
2026-03-10 11:12:21 +05:30
Hiren Kangad 1ffb7e9d41 fix(refboard): remove LRU eviction — let culling system manage textures
LRU eviction was destroying texture sources while sprites still
referenced them, causing PixiJS v8 'alphaMode of null' crash.

The viewport culling system (MAX_LOADED_TEXTURES=60) already controls
what's loaded/unloaded, so TextureManager doesn't need its own eviction.
Simplified to a pure load/unload cache with no budget tracking.
2026-03-10 11:09:17 +05:30
Hiren Kangad 11a44f5271 fix(refboard): don't add Sprite to display tree until texture loaded
PixiJS v8 crashes with 'alphaMode of null' even when Sprite.visible
is false — it still traverses the display object. Fix: create the
Sprite lazily in loadTexture() only after a real texture is ready.
On unload, remove and destroy the Sprite entirely.
2026-03-10 11:05:46 +05:30
Hiren Kangad deb3c23bcf fix(refboard): cap loaded textures at 60 and prioritize by distance
When zoomed out, all items fall within the viewport so the culling
system tried to load everything at once. Now:
- Max 60 textures loaded at any time (sorted by distance to center)
- Max 5 new texture loads per tick (prevents spike)
- Items over budget or far away are unloaded immediately
2026-03-10 11:03:59 +05:30
Hiren Kangad b3a15726f1 fix(refboard): hide sprite when texture unloaded to prevent render error
PixiJS v8 crashes with 'Cannot read alphaMode of null' when rendering
a Sprite with Texture.EMPTY. Fix: set sprite.visible = false until a
real texture is loaded, re-hide on unload.
2026-03-10 11:02:20 +05:30
Hiren Kangad 21ee4b2601 feat(refboard): incremental sync — stop full-scene broadcast on edits
Replace broadcastSceneNow() with incremental element:update for all
property-modifying operations (align, arrange, flip, resize, rotate,
normalize, frame color). Full scene sync now only fires via debounced
fallback (500ms) for structural changes (add/remove/paste/group).

Changes:
- onCanvasChange() accepts optional changedIds for incremental sync
- SelectionToolbar, context menu, shortcuts all pass item IDs
- TransformBox.onDragEnd passes item IDs instead of full scene
- ShortcutContext, MenuContext, hook interfaces updated

This dramatically reduces sync traffic during normal editing — only
the changed elements are sent instead of the entire board state.
2026-03-10 10:59:04 +05:30
Hiren Kangad 86024464c2 feat(refboard): viewport-based texture loading with hysteresis
Images no longer load textures eagerly on creation. Instead, the
culling ticker checks viewport proximity every 200ms:
- Load margin: 1x screen size beyond viewport (preload before visible)
- Unload margin: 2x screen size (hysteresis prevents thrash during pan)

ImageSprite changes:
- Constructor no longer calls loadTexture() — deferred to culling system
- New unloadTexture() restores placeholder and frees GPU memory
- Exposed texture getter for clipboard resolution detection
- loaded field now public for culling system to check state

This should allow hundreds of images without GPU OOM, since only
nearby textures consume GPU memory at any time.
2026-03-10 10:53:51 +05:30
Hiren Kangad 451ec339a5 fix(refboard): prevent WebGL OOM on large scenes
- Skip thumbnail generation when scene has >50 items (extract.canvas
  on full viewport with many textures causes GPU OOM and context loss)
- Add WebGL context lost/restored handlers — auto-reload scene on recovery
- Reduce texture memory budget from 512MB to 256MB for GPU headroom
2026-03-10 10:45:14 +05:30
Hiren Kangad a2eea8f22f fix(refboard): dot grid behind canvas, paste at native size
- Make PixiJS canvas background transparent so SVG dot grid shows through
- Move dot grid SVG before canvas in DOM with zIndex 0 (behind images)
- Set wrapper div background to #1e1e1e
- Remove 600px cap on pasted images — preserve native dimensions
2026-03-10 10:40:27 +05:30
Hiren Kangad 2b708f50c6 fix(refboard): revert image size cap, keep Assets.unload() cleanup
Remove server-side Sharp resize and texture dimension cap — PixiJS
handles large textures natively. Keep the proper Assets.unload() fix
to prevent "TextureSource destroyed instead of unloaded" warnings.
2026-03-10 10:36:07 +05:30
Hiren Kangad 03727cc014 fix(refboard): cap texture dimensions and add server-side image resizing
Prevents WebGL OOM errors when loading 90+ items by:
- Capping texture requests to 2048px via ?w= query param
- Adding Sharp-based server-side resize in image proxy endpoint
- Using Assets.unload() instead of texture.destroy() for proper cleanup
- Reducing GPU memory budget from 512MB to 256MB
2026-03-10 10:34:42 +05:30
Hiren Kangad 38c457943a fix(refboard): reduce distribute H/V min selection from 3 to 2 2026-03-10 04:27:01 +05:30
Hiren Kangad a42d674737 feat(refboard): distribute H/V now normalizes sizes for uniform rows/columns
- Distribute H: normalizes height + arranges as row (uniform horizontal strip)
- Distribute V: normalizes width + arranges as column (uniform vertical strip)
- Also reduced min selection from 3 to 2 for both operations
2026-03-10 04:26:00 +05:30
Hiren Kangad 380207a4c0 feat(refboard): grid layout for multi-file drop instead of horizontal row
When dropping multiple images, arranges them in a square-ish grid
(cols = ceil(sqrt(count))) with 20px gaps, tracking per-column widths
and per-row heights for proper alignment.
2026-03-10 04:20:50 +05:30
Hiren Kangad cebe4a3eb9 fix(refboard): full sync audit — batch multi-drag, persist resize, sync lock
- Batch broadcastTransform: sends all selected items in one emit instead
  of throttle-dropping all but the first (multi-select drag now syncs all)
- TransformBox onDragEnd: persist+sync final state after resize/rotate
  (was only broadcasting ephemeral transforms, never the final position)
- Layer lock toggle: add missing onCanvasChange() call for sync+save
- Receiver handles both batched and legacy single-item transform formats
2026-03-10 04:16:14 +05:30
Hiren Kangad f0f124d1d2 fix(refboard): broadcast scene changes to other clients immediately
onCanvasChange now calls broadcastSceneNow() so rearrangement,
alignment, and other bulk operations sync to other tabs in real-time
instead of waiting for the next periodic sync or page reload.
2026-03-10 04:10:43 +05:30
Hiren Kangad 7da41e4bd0 fix(refboard): dot grid above canvas with stronger visibility
Grid must overlay the opaque PixiJS canvas (zIndex 1, pointerEvents none).
Bumped dot radius and alpha for better visibility while staying decorative.
2026-03-10 04:08:08 +05:30
Hiren Kangad 2405f89462 fix(refboard): render dot grid behind canvas, not on top
Moved grid SVG before PixiCanvas in DOM order and set zIndex: 0
so it renders behind all canvas content.
2026-03-10 04:03:38 +05:30
Hiren Kangad a46eb47323 fix(refboard): copy to clipboard at native image resolution (up to 4x)
Computes resolution multiplier from native texture size vs display size,
so a 2000px image shown at 400px on canvas copies close to 2000px.
Capped at 4x to avoid excessive memory usage.
2026-03-10 04:02:54 +05:30
Hiren Kangad cfd17c4c2e fix(refboard): place multi-dropped images side-by-side instead of overlapping
Images dropped together are now laid out horizontally with a 20px gap,
using each image's actual rendered width to calculate the next position.
2026-03-10 04:01:29 +05:30
Hiren Kangad e9509ef7b0 feat(refboard): canvas polish — transform box sync, dot grid, snap tuning, perf fixes
- Fix transform box not updating after alignment/arrangement/normalize/flip operations
  (shortcuts, context menu, and selection toolbar all fixed with DRY _opUpdate helper)
- Replace 100ms polling loop with event-driven viewport updates (moved + wheel-scroll)
- Add adaptive dot grid background that responds to zoom/pan
- Reduce snap guide threshold from 8px to 4px for subtler snapping
- Remove PresenceOverlay (remote selection highlighting) — too heavy for minimal benefit
- Offset multiple dropped images so they don't overlap
- Add new canvas modules: SnapGuides, clipboard, grouping, FrameSprite, DrawingSprite,
  LaserPointer, context-menu-items, SelectionToolbar, Minimap
- Extract Editor hooks into dedicated files (useBoardLoader, useCanvasSetup,
  useShortcutHandler, useLayerPanel, useSaveManager, useFollowMode)
- Sync improvements: real-time transform broadcast, board rooms, viewport sync
2026-03-10 03:58:53 +05:30
Hiren Kangad cfd7adf73d Add video controls overlay for selected VideoSprite
Floating HTML overlay with play/pause, seekbar, time display, and
mute toggle that appears when a single video element is selected.
Positioned at the bottom of the video using viewport.toScreen()
coordinate conversion, polled at 100ms for position and 250ms for
playback progress.
2026-03-10 02:51:26 +05:30
Hiren Kangad 8a32119997 Add inline text editing on double-click for RefBoard text elements
Introduces TextEditor utility that overlays a styled <textarea> matching
the PixiJS Text object's font, size, and color. Hides the PixiJS text
while editing, commits on blur/Enter, cancels on Escape, and updates
dimensions from measured bounds on save.
2026-03-10 02:50:47 +05:30
Hiren Kangad 9542daa6cc fix(refboard): coordinate space bugs + legacy texture fallback
- TransformBox: use item.data (world-space) instead of getBounds() (screen-space)
  for correct handle positioning regardless of viewport pan/zoom
- TransformBox: scale drag sensitivity relative to object size, not fixed 500px
- TransformBox: compensate drag deltas for viewport zoom level
- TransformBox: fix corner/edge handles to keep opposite edge fixed during resize
- SelectionManager: use world-space hit testing instead of screen-space getBounds()
- SelectionManager: fix rubber band selection to use world-space item bounds
- TextureManager: detect legacy asset keys (with file extension) vs new LOD keys
  and load original file directly for pre-migration images (GPU handles scaling)
2026-03-09 23:45:58 +05:30
Hiren Kangad 21b2a433c7 InboxZone: a PixiJS Container positioned at (-500,-500) with a
dashed border and "Inbox" label. Auto-synced media slides in with
spring animations and random offset/rotation for a natural look.

MattermostImport: modal dialog for pulling images from a MM thread
URL and managing linked channels for auto-sync. Uses the existing
dark modal styling pattern from ShortcutsHelp.

Editor wiring: toolbar button for MM import, InboxZone added to
viewport on init, socket listener for board:media-arrived events
that routes assets into the InboxZone with a toast notification.
2026-03-09 23:36:04 +05:30
Hiren Kangad 51afce9674 Add viewport culling and LOD ticker to PixiCanvas
Runs a culling check every 200ms alongside the spring ticker.
Off-screen image sprites are hidden (visible=false), on-screen
ones get updateLOD() called with the current zoom level. Video
sprites get onVisibilityChange() to auto-play/pause based on
viewport visibility. Uses a 200px margin to avoid popping.
2026-03-09 23:35:52 +05:30
Hiren Kangad 9e4a427f97 Add drop shadow system with animated lift/drop during drag
DropShadowFilter applied to all ImageSprites for photos-on-a-desk feel.
SelectionManager now handles object dragging with shadow lift/drop and
spring-animated scale (1.0 <-> 1.03). Replaced incompatible @pixi/filter-drop-shadow
v5 with pixi-filters v6 for PixiJS v8 compatibility.
2026-03-09 23:35:02 +05:30
Hiren Kangad 86ae4dacd8 Add groupItems() and ungroupItems() with spring animations
Children converge ~25px toward combined center on group (gentle spring),
then reparent into a Container. Ungroup reparents back to viewport and
spreads children ~25px outward with matching animation.
2026-03-09 23:32:35 +05:30
Hiren Kangad 8cb287faf8 Integrate PixiCanvas into Editor, remove Fabric.js dependency
- Rewrite Editor.tsx: swap FabricCanvas for PixiCanvas, wire
  SelectionManager, SceneManager, and all Pixi-based APIs
- Port tools.ts from Fabric Canvas API to PixiJS Viewport/Scene
- Update LayerPanel type icons for PixiJS scene types (text, video)
- Remove FabricCanvas.tsx (dead code after swap)
- Uninstall fabric npm package
- All operations, shortcuts, sync, history, drag/drop, paste now
  flow through the PixiJS pipeline end-to-end
2026-03-09 23:29:25 +05:30
Hiren Kangad edcac21983 Port shortcut definitions from Fabric.js to PixiJS SceneManager/SelectionManager
Replace all Fabric.js Canvas references with SceneManager, SelectionManager,
UndoManager, and Viewport. Remove withBreak/breakSelection helper (no longer
needed without ActiveSelection). Implement z-swap layer ordering for ] and [
keys, clone-based paste/duplicate via SceneManager._createItem, and direct
history.undo()/redo() calls instead of undoRef indirection.
2026-03-09 23:23:10 +05:30
Hiren Kangad 9c5056d11e Port sync.ts to v2 format with SceneManager
Rewrite sync module to work with SceneManager and v2 scene format
instead of Fabric.js. Key changes:
- setupSync takes SceneManager instead of Canvas
- broadcastScene uses sceneManager.serialize() for v2 SceneData
- broadcastTransform reads from item.data (x,y,sx,sy,angle)
- onSceneReceived uses diff-based loadScene (no flicker, no full reload)
- onTransformReceived updates item.data + displayObject directly
- Remove _suppress/resumeBroadcasts (diff-based loading is safe)
- Remove userInteracting/pendingScene deferral (not needed with diffs)
- Remove all Fabric.js imports and types
2026-03-09 23:21:32 +05:30
Hiren Kangad 634937edc2 Port history.ts to v2 scene format with diff-based restore
Replace Fabric.js Canvas dependency with SceneManager. UndoManager now
serializes via sceneManager.serialize() and restores via loadScene()
with diff-based reconciliation (no flicker, no CORS workarounds).
2026-03-09 23:21:17 +05:30
Hiren Kangad 483d31a2eb Port operations.ts from Fabric.js to PixiJS SceneItem API
Replace all FabricObject usage with SceneItem, accessing item.data.x/y/w/h/sx/sy
instead of obj.left/top/width*scaleX. Sync displayObject position/scale after
mutations. Remove breakSelection (no ActiveSelection needed), setCoords calls,
and all fabric imports. Toggle grayscale uses PixiJS ColorMatrixFilter.desaturate().
arrangeByZOrder now uses item.data.z directly instead of canvas object index.
2026-03-09 23:19:34 +05:30
Hiren Kangad 673df6af21 Port image drop & paste from Fabric.js to PixiJS
Replace Fabric Canvas/FabricImage/Rect with PixiJS Graphics and
pixi-viewport's toWorld() for coordinate mapping. Upload responses
now dispatch to SceneManager.addImageFromUpload() for images or
create VideoSprite directly for video media types based on the
backend's asset_key and media_type fields.
2026-03-09 23:19:33 +05:30
Hiren Kangad 7359d4c1b4 Add VideoSprite with auto-pause and concurrent video limit
Implements inline video playback as a PixiJS Sprite subclass with:
- MAX_CONCURRENT_VIDEOS (3) with oldest-eviction policy
- Visibility-based auto play/pause for viewport culling
- Play icon overlay, tap-to-toggle-mute interaction
- Proper resource cleanup on destroy (video element + texture)
2026-03-09 23:16:28 +05:30
Hiren Kangad c8382771de Add ImageSprite with LOD tier switching and lazy loading
Introduces ImageSprite (extends Sprite) that manages per-image LOD
texture swapping via TextureManager. Shows a placeholder rect until
the first texture loads, then upgrades/downgrades tiers based on zoom.
2026-03-09 23:15:40 +05:30
Hiren Kangad bef0778804 Add SelectionManager with click, shift-click, and rubber-band selection
Implements Task 3.1: manages the selected item set with hit testing
in reverse z-order, rubber-band rectangle selection on empty-space drag,
shift-click toggle, and viewport drag-pause during rubber banding.
Integrates with TransformBox to show handles on selection change.
2026-03-09 23:14:15 +05:30
Hiren Kangad f7a4e9cab1 Add TransformBox with resize/rotate handles for selected items
Implements Task 3.2: an interactive transform overlay that draws
8 resize handles (corners + edge midpoints) and a rotation handle
around the combined bounding rect of selected scene items. Each handle
supports pointer-drag to apply scale and rotation transforms.
2026-03-09 23:14:07 +05:30
Hiren Kangad 84f851ee81 Add PixiCanvas component as drop-in replacement for FabricCanvas
Implements the main React component wrapping PixiJS v8 Application and
pixi-viewport with: async init, viewport persistence per board,
space-to-pan, spring animation ticker, v1/v2 scene loading, ResizeObserver
resizing, and imperative handle (fitAll, zoom control, scene/viewport access).
2026-03-09 23:11:19 +05:30
Hiren Kangad 476020c0b7 Add SceneManager core with diff-based scene loading and spring animations
Central object model replacing Fabric.js canvas management. Implements
diff-based loadScene (reconcile existing/new/removed items), z-ordering,
spring-animated add/remove, and TextureManager-backed image loading.
2026-03-09 23:09:21 +05:30
Hiren Kangad 6c3666ed62 Add v2 scene format types and Fabric v1→v2 converter
Scene serialization layer for the PixiJS migration: defines SceneData v2
types (Image/Video/Text/Group objects) and convertFabricToV2() which
transforms existing Fabric canvas JSON to the new format. Handles asset
key extraction from /api/images/ URLs, group flattening with coordinate
transforms, filter extraction, and edge cases for missing fields.
2026-03-09 23:07:09 +05:30
Hiren Kangad 87371d0749 Add TextureManager with LRU eviction and 512MB memory budget
GPU texture cache keyed by asset:tier that loads textures on demand
via PixiJS Assets.load(), estimates RGBA memory usage, and evicts
least-recently-used entries when the budget is exceeded.
2026-03-09 23:06:04 +05:30
Hiren Kangad d7766a027a Add spring physics engine for GPU-driven animations
Damped harmonic oscillator with configurable stiffness, damping, mass,
and precision. Includes four presets (drop, gentle, snappy, bounce) and
a SpringManager to batch-tick and auto-remove settled springs.
2026-03-09 23:04:48 +05:30
Hiren Kangad 5f40d5c0d3 Install PixiJS v8 dependencies for canvas migration
Adds pixi.js@^8, pixi-viewport, and @pixi/filter-drop-shadow
to RefBoard frontend as part of the Fabric.js to PixiJS migration.
2026-03-09 23:04:33 +05:30