fix: resolve 41 bugs and wire 21 unimplemented features in image editor

Canvas rendering:
- Fix Konva filter application order (filters before cache)
- Implement 6 missing filters (motionBlur, radialBlur, surfaceBlur, vignette, grain, sharpen)
- Implement exposure, vibrance, warmth adjustments as custom Konva filters
- Apply layer blend modes via globalCompositeOperation
- Apply object effects (drop shadow, outer glow, stroke) to all shapes
- Mount SmartGuidesOverlay during move tool drag
- Clip pixel grid to visible viewport (200-line cap for performance)

Store logic:
- resizeImage now scales all objects proportionally (points, radii, fontSize)
- rotate/flip/trim handle line/arrow points arrays and center-based objects
- applyCrop creates cropped source image via offscreen canvas
- invertSelection creates mask from bounds when no mask exists
- cutObjects uses single atomic set() to prevent race conditions
- sendToBack respects layer ordering in multi-layer documents
- Add batchNudge() and commitHistory() for undoable nudge operations
- Add updateLayerThumbnail() method

Tool hooks:
- Fix clone stamp/dodge/burn perf (toDataURL only on mouseUp, not every move)
- Fix magic wand zoom/pixelRatio with explicit stage.toCanvas() viewport
- Fix eyedropper sampling with unzoomed canvas export
- Fix selection tool stale closure via isDrawingRef
- Implement polygonal lasso (click-to-place vertices, double-click to close)
- Implement selection subtract mode (geometric and mask-based)
- Implement gradient live preview during drag
- Fix transform/move tool to persist changes and handle ellipse/polygon/star

UI wiring:
- Mount rulers and guidelines in editor page
- Wire histogram with live canvas imageData
- Wire autosave recovery with blob-to-dataURL conversion
- Wire fill dialog to Shift+Backspace shortcut
- Wire eyedropper and transform options to options bar
- Fix history panel undo/redo button reactive state via useSyncExternalStore
- Fix layer row name click to select layer (timer-based click/dblclick)
- Fix zoom animation coordinate drift with progressive store sync
- Fix copy merged to use Konva stage composite export

Tests:
- 49 new unit tests (store fixes + konva filters)
- 8 new E2E test files with 39 test cases
This commit is contained in:
SnapOtter
2026-05-08 16:43:27 +08:00
parent 3a2b1ee105
commit dd73a8a50a
30 changed files with 3404 additions and 203 deletions
@@ -204,14 +204,49 @@ export function TransformToolTransformer({
const handleTransformEnd = useCallback(
(e: Konva.KonvaEventObject<Event>) => {
const node = e.target;
const id = node.id();
const scaleX = node.scaleX();
const scaleY = node.scaleY();
const newW = Math.max(1, node.width() * scaleX);
const newH = Math.max(1, node.height() * scaleY);
// Reset scale on the Konva node
node.scaleX(1);
node.scaleY(1);
node.width(newW);
node.height(newH);
// Directly update the store with the new attributes
if (id) {
const obj = useEditorStore.getState().objects.find((o) => o.id === id);
const newAttrs: Record<string, unknown> = {
x: node.x(),
y: node.y(),
rotation: node.rotation(),
};
if (obj?.type === "ellipse") {
// Convert width/height to radiusX/radiusY for ellipses
newAttrs.radiusX = newW / 2;
newAttrs.radiusY = newH / 2;
} else if (obj?.type === "polygon") {
// Convert width/height to radius for polygons
newAttrs.radius = (newW + newH) / 4;
} else if (obj?.type === "star") {
// Scale outerRadius and innerRadius proportionally for stars
const starAttrs = obj.attrs as { outerRadius: number; innerRadius: number };
const avgScale = (Math.abs(scaleX) + Math.abs(scaleY)) / 2;
newAttrs.outerRadius = starAttrs.outerRadius * avgScale;
newAttrs.innerRadius = starAttrs.innerRadius * avgScale;
} else {
// Rectangles, images, text: use width/height directly
newAttrs.width = newW;
newAttrs.height = newH;
}
useEditorStore.getState().updateObject(id, newAttrs);
}
onTransformEnd?.(e);
},
[onTransformEnd],