feat: text tool UX, image crop, text format toolbar, double-click focus

- Text tool: click-to-place immediately opens inline editor, auto-switches
  back to select tool. Empty text cleanup on save.
- Crop: select image + press C (or right-click > Crop) to enter crop mode.
  8 drag handles with rule-of-thirds grid, dimmed outside area.
  Enter confirms, Escape cancels. Non-destructive (stored as normalized rect).
- Text format toolbar: appears when text items selected, with font size +/-,
  font family dropdown, and color picker with presets.
- Double-click image: zoom-to-fit (PureRef-style focus)
- Entrance animation: replaced bounce with simple fade-in (no delay before
  items become interactive)
- TextEditor: allow empty text (caller handles cleanup)
This commit is contained in:
Hiren Kangad
2026-03-11 17:22:25 +05:30
parent 4ed475c62e
commit 58da934ec1
14 changed files with 785 additions and 50 deletions
+33 -1
View File
@@ -13,6 +13,7 @@ import type { SelectionManager } from './SelectionManager';
import { Text, TextStyle } from 'pixi.js';
import { DrawingSprite } from './sprites/DrawingSprite';
import type { DrawingObject } from './scene-format';
import { TextEditor } from './TextEditor';
export enum ToolType {
SELECT = 'SELECT',
@@ -44,6 +45,10 @@ export interface ToolContext {
onChange: () => void;
/** Broadcast only specific changed elements (lightweight, for live drawing). */
broadcastElements?: (ids: string[]) => void;
/** Text editor instance for inline editing. */
textEditor?: TextEditor;
/** Switch back to select tool after placing text. */
switchToSelect?: () => void;
}
export function activateTool(
@@ -95,7 +100,7 @@ export function activateTool(
locked: false,
name: '',
visible: true,
text: 'Type here',
text: ' ', // placeholder — will be replaced by user input
fontSize: opts.fontSize!,
fill: opts.color!,
fontFamily: 'sans-serif',
@@ -106,6 +111,33 @@ export function activateTool(
ctx.broadcastElements?.([textData.id]);
ctx.onChange();
// Immediately open inline editor on the new text item
const item = scene.getById(textData.id);
if (item && ctx.textEditor) {
// Find the canvas DOM container
const canvasEl = container.querySelector('canvas');
const domContainer = canvasEl?.parentElement ?? container;
// Small delay to let PixiJS render the text sprite
setTimeout(() => {
ctx.textEditor!.startEditing(item, viewport, domContainer, () => {
// If user saved empty text, remove the item
const text = (item.data as any).text?.trim();
if (!text) {
scene.removeItem(item.id, true);
}
ctx.broadcastElements?.([item.id]);
ctx.onChange();
});
// Clear placeholder so user types from scratch
const ta = document.querySelector('textarea');
if (ta) { ta.value = ''; }
}, 50);
}
// Switch back to select tool after placing
ctx.switchToSelect?.();
// Remove handler after placing text
container.removeEventListener('pointerdown', onClick);
};