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:
@@ -1,5 +1,6 @@
|
||||
import { Container, Sprite, Texture, Graphics } from "pixi.js";
|
||||
import { Container, Sprite, Texture, Graphics, Rectangle } from "pixi.js";
|
||||
import { TextureManager } from "../TextureManager";
|
||||
import type { CropRect } from "../scene-format";
|
||||
|
||||
/**
|
||||
* A Container holding a shadow graphic + sprite with lazy texture loading.
|
||||
@@ -24,6 +25,7 @@ export class ImageSprite extends Container {
|
||||
private placeholder: Graphics | null = null;
|
||||
private _sprite: Sprite | null = null;
|
||||
private _shadow: Graphics;
|
||||
private _cropMask: Graphics | null = null;
|
||||
private _naturalWidth: number;
|
||||
private _naturalHeight: number;
|
||||
|
||||
@@ -54,9 +56,14 @@ export class ImageSprite extends Container {
|
||||
// NOTE: Sprite is created lazily in loadTexture() — NOT added here.
|
||||
}
|
||||
|
||||
get naturalWidth(): number { return this._naturalWidth; }
|
||||
get naturalHeight(): number { return this._naturalHeight; }
|
||||
|
||||
private _drawShadow(cfg: { offsetX: number; offsetY: number; alpha: number }): void {
|
||||
const w = this._naturalWidth;
|
||||
const h = this._naturalHeight;
|
||||
this._shadow.clear();
|
||||
this._shadow.rect(cfg.offsetX, cfg.offsetY, this._naturalWidth, this._naturalHeight);
|
||||
this._shadow.rect(cfg.offsetX, cfg.offsetY, w, h);
|
||||
this._shadow.fill({ color: 0x000000, alpha: cfg.alpha });
|
||||
}
|
||||
|
||||
@@ -75,6 +82,48 @@ export class ImageSprite extends Container {
|
||||
return this._sprite?.texture ?? Texture.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a crop mask. Normalized 0-1 values relative to natural dimensions.
|
||||
* Pass null/undefined to remove crop.
|
||||
*/
|
||||
applyCrop(crop: CropRect | undefined): void {
|
||||
if (!crop) {
|
||||
// Remove crop
|
||||
if (this._cropMask) {
|
||||
if (this._sprite) this._sprite.mask = null;
|
||||
this.removeChild(this._cropMask);
|
||||
this._cropMask.destroy();
|
||||
this._cropMask = null;
|
||||
}
|
||||
// Restore shadow to full size
|
||||
this._drawShadow(SHADOW_REST);
|
||||
return;
|
||||
}
|
||||
|
||||
const px = crop.x * this._naturalWidth;
|
||||
const py = crop.y * this._naturalHeight;
|
||||
const pw = crop.w * this._naturalWidth;
|
||||
const ph = crop.h * this._naturalHeight;
|
||||
|
||||
if (!this._cropMask) {
|
||||
this._cropMask = new Graphics();
|
||||
this.addChild(this._cropMask);
|
||||
}
|
||||
|
||||
this._cropMask.clear();
|
||||
this._cropMask.rect(px, py, pw, ph);
|
||||
this._cropMask.fill(0xffffff);
|
||||
|
||||
if (this._sprite) {
|
||||
this._sprite.mask = this._cropMask;
|
||||
}
|
||||
|
||||
// Update shadow to match cropped area
|
||||
this._shadow.clear();
|
||||
this._shadow.rect(px + SHADOW_REST.offsetX, py + SHADOW_REST.offsetY, pw, ph);
|
||||
this._shadow.fill({ color: 0x000000, alpha: SHADOW_REST.alpha });
|
||||
}
|
||||
|
||||
/** Load the full-res texture. Called by viewport culling when near viewport. */
|
||||
async loadTexture(): Promise<void> {
|
||||
if (this.loaded || this.loading) return;
|
||||
@@ -93,6 +142,11 @@ export class ImageSprite extends Container {
|
||||
this.addChild(sprite);
|
||||
this.loaded = true;
|
||||
|
||||
// Apply crop mask if one exists
|
||||
if (this._cropMask) {
|
||||
sprite.mask = this._cropMask;
|
||||
}
|
||||
|
||||
// Remove placeholder after successful load
|
||||
if (this.placeholder) {
|
||||
this.removeChild(this.placeholder);
|
||||
@@ -117,6 +171,7 @@ export class ImageSprite extends Container {
|
||||
|
||||
// Remove sprite from display tree entirely — avoids PixiJS v8 render crash
|
||||
if (this._sprite) {
|
||||
this._sprite.mask = null;
|
||||
this.removeChild(this._sprite);
|
||||
this._sprite.destroy();
|
||||
this._sprite = null;
|
||||
|
||||
Reference in New Issue
Block a user