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
This commit is contained in:
@@ -7,8 +7,9 @@
|
||||
|
||||
import { Container, Graphics, FederatedPointerEvent } from 'pixi.js';
|
||||
import type { Viewport } from 'pixi-viewport';
|
||||
import { SceneManager, SceneItem, getItemWorldBounds, isGroupChild } from './SceneManager';
|
||||
import { SceneManager, SceneItem, getItemWorldBounds } from './SceneManager';
|
||||
import { TransformBox } from './TransformBox';
|
||||
import { SnapGuides } from './SnapGuides';
|
||||
import { ImageSprite } from './sprites/ImageSprite';
|
||||
import { VideoSprite } from './sprites/VideoSprite';
|
||||
|
||||
@@ -31,6 +32,7 @@ const DRAG_THRESHOLD = 5; // px in screen space before object drag activa
|
||||
export class SelectionManager {
|
||||
readonly selectedIds: Set<string> = new Set();
|
||||
readonly transformBox: TransformBox;
|
||||
private _snapGuides: SnapGuides;
|
||||
|
||||
private _viewport: Viewport;
|
||||
private _scene: SceneManager;
|
||||
@@ -62,6 +64,8 @@ export class SelectionManager {
|
||||
|
||||
private _enabled = true;
|
||||
|
||||
get snapGuides(): SnapGuides { return this._snapGuides; }
|
||||
|
||||
constructor(viewport: Viewport, scene: SceneManager) {
|
||||
this._viewport = viewport;
|
||||
this._scene = scene;
|
||||
@@ -81,6 +85,9 @@ export class SelectionManager {
|
||||
this.transformBox.setViewport(viewport);
|
||||
this._overlay.addChild(this.transformBox);
|
||||
|
||||
// Snap guides
|
||||
this._snapGuides = new SnapGuides(scene, this._overlay);
|
||||
|
||||
// Bind events on viewport
|
||||
viewport.on('pointerdown', this._onPointerDown, this);
|
||||
viewport.on('globalpointermove', this._onPointerMove, this);
|
||||
@@ -229,17 +236,31 @@ export class SelectionManager {
|
||||
|
||||
// Lift shadow + spring scale on all selected image sprites
|
||||
this._applyLift();
|
||||
|
||||
// Begin snap guide session
|
||||
this._snapGuides.beginSession(this.selectedIds);
|
||||
}
|
||||
|
||||
if (this._objectDragging) {
|
||||
const currentWorld = this._viewport.toWorld(e.global.x, e.global.y);
|
||||
const ddx = currentWorld.x - this._lastDragWorldX;
|
||||
const ddy = currentWorld.y - this._lastDragWorldY;
|
||||
let ddx = currentWorld.x - this._lastDragWorldX;
|
||||
let ddy = currentWorld.y - this._lastDragWorldY;
|
||||
this._lastDragWorldX = currentWorld.x;
|
||||
this._lastDragWorldY = currentWorld.y;
|
||||
|
||||
// Move all selected items by delta and broadcast transforms
|
||||
// Compute combined bounds of selected items after applying delta
|
||||
const selected = this.getSelectedItems();
|
||||
const prospective = this._getSelectionBounds(selected);
|
||||
prospective.x += ddx;
|
||||
prospective.y += ddy;
|
||||
|
||||
// Snap to alignment guides
|
||||
const snap = this._snapGuides.computeSnap(prospective, this._viewport);
|
||||
ddx += snap.dx;
|
||||
ddy += snap.dy;
|
||||
this._snapGuides.drawGuides(snap.guides, this._viewport);
|
||||
|
||||
// Move all selected items by corrected delta and broadcast transforms
|
||||
for (const item of selected) {
|
||||
item.displayObject.x += ddx;
|
||||
item.displayObject.y += ddy;
|
||||
@@ -274,6 +295,7 @@ export class SelectionManager {
|
||||
if (this._objectDragging) {
|
||||
// End object drag — drop shadow + spring scale back
|
||||
this._applyDrop();
|
||||
this._snapGuides.endSession();
|
||||
this._objectDragging = false;
|
||||
|
||||
// Resume viewport drag
|
||||
@@ -390,6 +412,26 @@ export class SelectionManager {
|
||||
this._emitChange();
|
||||
}
|
||||
|
||||
// -- Selection Bounds Helper -----------------------------------------------
|
||||
|
||||
/** Compute the combined world bounding rect of the given items. */
|
||||
private _getSelectionBounds(items: SceneItem[]): { x: number; y: number; w: number; h: number } {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const item of items) {
|
||||
const { x, y, w, h } = getItemWorldBounds(item);
|
||||
if (x < minX) minX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (x + w > maxX) maxX = x + w;
|
||||
if (y + h > maxY) maxY = y + h;
|
||||
}
|
||||
|
||||
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
||||
}
|
||||
|
||||
// -- Change Notification --------------------------------------------------
|
||||
|
||||
private _emitChange(): void {
|
||||
|
||||
Reference in New Issue
Block a user