diff --git a/frontend/src/canvas/CropOverlay.ts b/frontend/src/canvas/CropOverlay.ts index 9fae4bd..b6b8c9e 100644 --- a/frontend/src/canvas/CropOverlay.ts +++ b/frontend/src/canvas/CropOverlay.ts @@ -11,6 +11,7 @@ import type { Viewport } from 'pixi-viewport'; import type { SceneItem } from './SceneManager'; import type { ImageObject, CropRect } from './scene-format'; import { getImageViewRectWorldCorners, imageViewPointToWorld, worldToImageViewPoint } from './imageTransforms'; +import { ImageSprite } from './sprites/ImageSprite'; const HANDLE_SIZE = 8; const HANDLE_HIT_SIZE = 22; @@ -35,6 +36,7 @@ export class CropOverlay extends Container { private _border: Graphics; private _handles = new Map(); 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 _onConfirm: ((item: SceneItem, crop: CropRect) => void) | null = null; private _onCancel: (() => void) | null = null; @@ -91,7 +93,11 @@ export class CropOverlay extends Container { if (item.type !== 'image') return; this._item = item; 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 }; + if (item.displayObject instanceof ImageSprite) { + item.displayObject.applyCrop(undefined); + } this._viewport.plugins.pause('drag'); this.visible = true; this._onStateChange?.(true); @@ -121,6 +127,9 @@ export class CropOverlay extends Container { /** Cancel cropping — restore original state. */ cancel(): void { + if (this._item?.displayObject instanceof ImageSprite) { + this._item.displayObject.applyCrop(this._originalCrop); + } this._cleanup(); this._onCancel?.(); } @@ -132,6 +141,7 @@ export class CropOverlay extends Container { this._removeDomDragListeners(); this._viewport.plugins.resume('drag'); this._item = null; + this._originalCrop = undefined; this._drag = null; this.visible = false; this._onStateChange?.(false); diff --git a/frontend/src/hooks/useCanvasSetup.ts b/frontend/src/hooks/useCanvasSetup.ts index 14bcf65..29104c6 100644 --- a/frontend/src/hooks/useCanvasSetup.ts +++ b/frontend/src/hooks/useCanvasSetup.ts @@ -16,6 +16,7 @@ import { CropOverlay } from '../canvas/CropOverlay'; import { MarkdownOverlay } from '../canvas/MarkdownOverlay'; import { TextSprite } from '../canvas/sprites/TextSprite'; import { TextSharpnessManager } from '../canvas/textSharpness'; +import { getItemWorldBounds } from '../canvas/SceneManager'; // PresenceOverlay removed — remote selection highlighting was too heavy for minimal benefit import { connectSocket, disconnectSocket } from '../socket'; import api from '../api'; @@ -150,13 +151,14 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { const padding = 80; // screen pixels of padding around the image const screenW = viewport.screenWidth; const screenH = viewport.screenHeight; - const bw = item.data.w * Math.abs(item.data.sx); - const bh = item.data.h * Math.abs(item.data.sy); + const bounds = getItemWorldBounds(item); + const bw = bounds.w; + const bh = bounds.h; const scaleX = (screenW - padding * 2) / bw; const scaleY = (screenH - padding * 2) / bh; const targetScale = Math.min(scaleX, scaleY, 3); // cap at 3x - const cx = item.data.x + bw / 2; - const cy = item.data.y + bh / 2; + const cx = bounds.x + bw / 2; + const cy = bounds.y + bh / 2; viewport.animate({ time: 300, position: { x: cx, y: cy }, @@ -481,7 +483,9 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { if (item.displayObject instanceof ImageSprite) { item.displayObject.applyCrop(imgData.crop); } + scene.updateSpatialEntry(item); selection.setEnabled(true); + selection.transformBox.update([item]); onCanvasChange([item.id]); };