refboard: improve crop preview and cropped bounds focus

This commit is contained in:
Hiren Kangad
2026-03-13 12:15:22 +05:30
parent 4f8e3984b2
commit 5c13def2d2
2 changed files with 18 additions and 4 deletions
+10
View File
@@ -11,6 +11,7 @@ import type { Viewport } from 'pixi-viewport';
import type { SceneItem } from './SceneManager'; import type { SceneItem } from './SceneManager';
import type { ImageObject, CropRect } from './scene-format'; import type { ImageObject, CropRect } from './scene-format';
import { getImageViewRectWorldCorners, imageViewPointToWorld, worldToImageViewPoint } from './imageTransforms'; import { getImageViewRectWorldCorners, imageViewPointToWorld, worldToImageViewPoint } from './imageTransforms';
import { ImageSprite } from './sprites/ImageSprite';
const HANDLE_SIZE = 8; const HANDLE_SIZE = 8;
const HANDLE_HIT_SIZE = 22; const HANDLE_HIT_SIZE = 22;
@@ -35,6 +36,7 @@ export class CropOverlay extends Container {
private _border: Graphics; private _border: Graphics;
private _handles = new Map<HandleId, Graphics>(); private _handles = new Map<HandleId, Graphics>();
private _crop: CropRect = { x: 0, y: 0, w: 1, h: 1 }; private _crop: CropRect = { x: 0, y: 0, w: 1, h: 1 };
private _originalCrop: CropRect | undefined;
private _drag: { mode: DragMode; startCrop: CropRect; startPoint: { x: number; y: number } } | null = null; private _drag: { mode: DragMode; startCrop: CropRect; startPoint: { x: number; y: number } } | null = null;
private _onConfirm: ((item: SceneItem, crop: CropRect) => void) | null = null; private _onConfirm: ((item: SceneItem, crop: CropRect) => void) | null = null;
private _onCancel: (() => void) | null = null; private _onCancel: (() => void) | null = null;
@@ -91,7 +93,11 @@ export class CropOverlay extends Container {
if (item.type !== 'image') return; if (item.type !== 'image') return;
this._item = item; this._item = item;
const imgData = item.data as ImageObject; const imgData = item.data as ImageObject;
this._originalCrop = imgData.crop ? { ...imgData.crop } : undefined;
this._crop = imgData.crop ? { ...imgData.crop } : { x: 0, y: 0, w: 1, h: 1 }; this._crop = imgData.crop ? { ...imgData.crop } : { x: 0, y: 0, w: 1, h: 1 };
if (item.displayObject instanceof ImageSprite) {
item.displayObject.applyCrop(undefined);
}
this._viewport.plugins.pause('drag'); this._viewport.plugins.pause('drag');
this.visible = true; this.visible = true;
this._onStateChange?.(true); this._onStateChange?.(true);
@@ -121,6 +127,9 @@ export class CropOverlay extends Container {
/** Cancel cropping — restore original state. */ /** Cancel cropping — restore original state. */
cancel(): void { cancel(): void {
if (this._item?.displayObject instanceof ImageSprite) {
this._item.displayObject.applyCrop(this._originalCrop);
}
this._cleanup(); this._cleanup();
this._onCancel?.(); this._onCancel?.();
} }
@@ -132,6 +141,7 @@ export class CropOverlay extends Container {
this._removeDomDragListeners(); this._removeDomDragListeners();
this._viewport.plugins.resume('drag'); this._viewport.plugins.resume('drag');
this._item = null; this._item = null;
this._originalCrop = undefined;
this._drag = null; this._drag = null;
this.visible = false; this.visible = false;
this._onStateChange?.(false); this._onStateChange?.(false);
+8 -4
View File
@@ -16,6 +16,7 @@ import { CropOverlay } from '../canvas/CropOverlay';
import { MarkdownOverlay } from '../canvas/MarkdownOverlay'; import { MarkdownOverlay } from '../canvas/MarkdownOverlay';
import { TextSprite } from '../canvas/sprites/TextSprite'; import { TextSprite } from '../canvas/sprites/TextSprite';
import { TextSharpnessManager } from '../canvas/textSharpness'; import { TextSharpnessManager } from '../canvas/textSharpness';
import { getItemWorldBounds } from '../canvas/SceneManager';
// PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit // PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit
import { connectSocket, disconnectSocket } from '../socket'; import { connectSocket, disconnectSocket } from '../socket';
import api from '../api'; import api from '../api';
@@ -150,13 +151,14 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
const padding = 80; // screen pixels of padding around the image const padding = 80; // screen pixels of padding around the image
const screenW = viewport.screenWidth; const screenW = viewport.screenWidth;
const screenH = viewport.screenHeight; const screenH = viewport.screenHeight;
const bw = item.data.w * Math.abs(item.data.sx); const bounds = getItemWorldBounds(item);
const bh = item.data.h * Math.abs(item.data.sy); const bw = bounds.w;
const bh = bounds.h;
const scaleX = (screenW - padding * 2) / bw; const scaleX = (screenW - padding * 2) / bw;
const scaleY = (screenH - padding * 2) / bh; const scaleY = (screenH - padding * 2) / bh;
const targetScale = Math.min(scaleX, scaleY, 3); // cap at 3x const targetScale = Math.min(scaleX, scaleY, 3); // cap at 3x
const cx = item.data.x + bw / 2; const cx = bounds.x + bw / 2;
const cy = item.data.y + bh / 2; const cy = bounds.y + bh / 2;
viewport.animate({ viewport.animate({
time: 300, time: 300,
position: { x: cx, y: cy }, position: { x: cx, y: cy },
@@ -481,7 +483,9 @@ export function useCanvasSetup(deps: CanvasSetupDeps) {
if (item.displayObject instanceof ImageSprite) { if (item.displayObject instanceof ImageSprite) {
item.displayObject.applyCrop(imgData.crop); item.displayObject.applyCrop(imgData.crop);
} }
scene.updateSpatialEntry(item);
selection.setEnabled(true); selection.setEnabled(true);
selection.transformBox.update([item]);
onCanvasChange([item.id]); onCanvasChange([item.id]);
}; };