Commit Graph
172 Commits
Author SHA1 Message Date
Hiren Kangad b6b615957b feat: show queued upload count in panel header 2026-03-10 20:51:25 +05:30
Hiren Kangad 9dc0815a12 feat: upload queue with queued status and cancel button
Multi-file drops now show queued/uploading distinction. Jobs start
as 'queued' and transition to 'uploading' when the HTTP request
begins. Queued items show a cancel button in the upload panel.
Cancelled jobs are skipped in the sequential upload loop.
2026-03-10 20:42:17 +05:30
Hiren Kangad 9af4051b67 fix: release GIF ref on sprite destroy to prevent gifCache leak
Override destroy() in AnimatedGifSprite to call releaseGif() when
a loaded GIF item is deleted from the board. Without this, the
gifCache refCount stays elevated until full canvas teardown.
2026-03-10 20:09:24 +05:30
Hiren Kangad 1f5ab883ce fix: file drop uses extension fallback when MIME type is empty
Browsers sometimes report empty or non-standard MIME types for
dragged files. Now checks file extension as fallback when MIME
doesn't match the allowlist. Only shows rejection feedback for
files that are recognizably media but unsupported.
2026-03-10 20:03:18 +05:30
Hiren Kangad bc2c5fb974 fix: reject unsupported media URL drops with feedback
URLs with media-like extensions (.avif, .tiff, .heic, .bmp, etc.)
that aren't in our supported set now show a failed entry in the
upload panel. Plain webpage URLs without media extensions are still
silently ignored (not our concern).
2026-03-10 19:49:05 +05:30
Hiren Kangad 065762d273 fix: synthesize readable filename for rejected clipboard pastes
Rejected clipboard items now show as "pasted-item.tiff" instead
of raw MIME subtype, so the upload panel entry is readable.
2026-03-10 19:46:17 +05:30
Hiren Kangad 45891a278c fix: reject unsupported file types with explicit feedback
Replace broad image/*/video/* prefix matching with explicit MIME
allowlist matching backend. Unsupported files (BMP, TIFF, AVIF,
HEIC, PDF, etc.) now show as failed rows in the upload panel
with a clear "Unsupported format: .ext" message instead of being
silently ignored. Applies to both drag-drop and clipboard paste.
2026-03-10 19:44:04 +05:30
Hiren Kangad e45fce3e67 fix: harden GIF support — ref-counted sources, defensive detection, play budget
1. Move GIF loading behind TextureManager.loadGif/releaseGif with
   ref-counting (fixes shared-source invalidation on duplicate GIFs)
2. Strip query strings/fragments before .gif extension check
3. GIFs now load paused — separate MAX_PLAYING_GIFS=8 budget in
   culling system, only nearest N animate (others show static frame)
2026-03-10 19:41:50 +05:30
Hiren Kangad 5a64708bd4 feat: add animated GIF support using PixiJS built-in GifSprite
Register GifAsset extension for .gif loading, create AnimatedGifSprite
wrapper with lazy load/unload matching ImageSprite pattern, and detect
.gif assets in SceneManager to use animated rendering. GIFs stored as
type 'image' in scene format — no schema changes needed.
2026-03-10 19:26:41 +05:30
Hiren Kangad 51e53a323b feat: add export-as-image with format, scale, and background options
New ExportDialog component with scope (selection/all), filename,
format (PNG/JPEG/WebP), quality slider, scale multiplier, and
background color picker. Wired into Toolbar and Editor.
2026-03-10 19:16:05 +05:30
Hiren Kangad c126885e53 fix: upload manager — URL imports, active count, board reset
1. URL imports now participate in upload manager pipeline:
   addUrlJob() creates a job row, uploadComplete/setFailed called
   on success/failure — consistent with local file uploads

2. activeCount only counts uploading/processing jobs, not failed.
   Failed jobs no longer inflate the "Uploads (n)" header count.

3. uploadManager.clear() called on boardId change so stale jobs
   from a previous board don't linger in the next board's panel.
2026-03-10 19:10:14 +05:30
Hiren Kangad 784554e40d fix: video size error handling — validation, timeouts, failure surfacing
Backend:
- Increase ffmpeg/ffprobe timeouts from 15s to 60s for large videos
- Classify processing errors (timeout, OOM, corrupt) with user-facing messages
- Emit failure details via media:job:update socket event (error field)
- Bump refboard container memory 512M → 1G

Frontend:
- Client-side file size validation (200MB) before upload starts
- Oversized files show immediate error in upload manager, skip upload
- Handle media:job:update status='failed' — surface error in upload panel
- Add processingFailed() to UploadManager for video processing errors
2026-03-10 19:04:42 +05:30
Hiren Kangad 38fb3edf14 feat: add Upload Manager with progress tracking and status panel
- UploadManager store: tracks jobs through uploading → processing → done/failed
- UploadPanel component: floating bottom-left widget showing upload progress,
  file names, sizes, status with auto-dismiss for completed items
- axios onUploadProgress wired for real-time upload percentage
- Video jobs transition to "processing" after upload, then "done" when
  media:job:update socket event arrives (reuses existing pipeline)
- Failed uploads show error message from server response
- Clear button to dismiss finished/failed items
2026-03-10 18:56:25 +05:30
Hiren Kangad 26527006a6 feat: auto-select imported items after upload
Pass SelectionManager to setupDragDrop and setupPaste. After each
upload completes, the newly created scene items are selected:
- Single file/URL: selectOnly
- Multi-file drop/paste: select all new items
2026-03-10 18:51:28 +05:30
Hiren Kangad de418b2ba5 feat: media processing pipeline, spatial indexing, canvas-based video rendering
- Background media worker: polls media_jobs table, runs ffprobe+ffmpeg
  with concurrency limit, generates video posters, emits socket events
- Non-blocking video upload: stores file + enqueues job, returns immediately
- Poster hydration on board load: GET /boards/:id injects poster/dimensions
  from DB into canvas_state video objects
- SpatialGrid: fixed-cell (512px) spatial hash for O(nearby) culling instead
  of O(all) item scanning, eliminates setTimeout violations
- Canvas-based video rendering: draws video frames to offscreen canvas then
  uploads to GPU, completely eliminates GL_INVALID_OPERATION errors from
  PixiJS VideoSource auto-update mechanism
- Server poster upgrade path: culling ticker and applyProcessedMedia() both
  upgrade client-captured posters to server posters when available
- Pause restores server poster (paused video behaves like an image)
- Selection drag-end persistence: onObjectDragEnd broadcasts + saves + undo
- Live media:job:update socket handler patches scene data + VideoSprite
  dimensions without broadcast fanout
2026-03-10 14:30:01 +05:30
Hiren Kangad fdcee3536f fix(refboard): guard loadServerPoster against repeated culling calls
Add _serverPosterLoading flag so repeated 200ms culling ticks don't
call textures.load() multiple times before the first resolves. Also
release the texture ref if the sprite was destroyed or started playing
during the async load.
2026-03-10 12:24:17 +05:30
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