mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve 50+ bugs across editor store, canvas, panels, and tools
- Store: loadImage resets state, applyCrop updates sourceImageSize, setTool clears stale cropState, layer ordering preserved across layers, rotateCanvas/flipCanvas account for object dimensions, trimCanvas implemented, resizeCanvas supports anchor positioning - Canvas: crop overlay interactive, move tool drag events wired, hand tool panning, stage ref effect mount-only, checkerboard tracks pan offset, filter cache clearing - Panels: hslToHex color fix for hue 240-360, adjustments single setAdjustment call, curves draggingIndex after sort, history panel fresh temporal reads, export transparent setting, canvas resize dialog re-sync, hex input respects picker target - Tools: text tool fixed positioning and event cleanup, shape tool deferred addObject, selection mode wired to store, magic wand tolerance controlled, eyedropper coordinate transform - Options: dodge-burn/shape/clone/fill/gradient/pixel-brush use proper store actions instead of raw setState - Added 20+ store actions for tool settings - Updated tests for corrected rotation/flip/trim behavior - Excluded e2e-editor from vitest config
This commit is contained in:
@@ -12,8 +12,6 @@ import type { AdjustmentValues } from "@/types/editor";
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
|
||||
const ADJUSTMENT_SLIDERS: {
|
||||
key: keyof AdjustmentValues;
|
||||
label: string;
|
||||
@@ -378,28 +376,13 @@ function AutoAdjustmentsSection() {
|
||||
function AdjustmentsSlidersSection() {
|
||||
const adjustments = useEditorStore((s) => s.adjustments);
|
||||
const setAdjustment = useEditorStore((s) => s.setAdjustment);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(key: keyof AdjustmentValues, value: number) => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setAdjustment(key, value);
|
||||
}, DEBOUNCE_MS);
|
||||
// Immediately set for responsive UI
|
||||
setAdjustment(key, value);
|
||||
},
|
||||
[setAdjustment],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{ADJUSTMENT_SLIDERS.map(({ key, label, min, max }) => (
|
||||
@@ -778,6 +761,8 @@ function CurvesSection() {
|
||||
const pts = [...prev[channel]];
|
||||
pts[draggingIndex] = pos;
|
||||
pts.sort((a, b) => a.x - b.x);
|
||||
const newIndex = pts.findIndex((p) => p.x === pos.x && p.y === pos.y);
|
||||
if (newIndex !== -1) setDraggingIndex(newIndex);
|
||||
return { ...prev, [channel]: pts };
|
||||
});
|
||||
},
|
||||
@@ -1058,15 +1043,24 @@ export function AdjustmentsPanel() {
|
||||
}, [adjustments, filters]);
|
||||
|
||||
const handleResetAll = useCallback(() => {
|
||||
resetAdjustments();
|
||||
// Reset all filters by toggling off any enabled ones
|
||||
const store = useEditorStore.getState();
|
||||
for (const f of store.filters) {
|
||||
if (f.enabled) {
|
||||
store.toggleFilter(f.type);
|
||||
}
|
||||
}
|
||||
}, [resetAdjustments]);
|
||||
useEditorStore.setState({
|
||||
adjustments: {
|
||||
brightness: 0,
|
||||
contrast: 0,
|
||||
hue: 0,
|
||||
saturation: 0,
|
||||
luminance: 0,
|
||||
exposure: 0,
|
||||
vibrance: 0,
|
||||
warmth: 0,
|
||||
},
|
||||
filters: store.filters.map((f) => ({ ...f, enabled: false })),
|
||||
isDirty: true,
|
||||
lastAction: "Reset All",
|
||||
_historyVersion: store._historyVersion + 1,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
// Bake adjustments and filters into pixel data
|
||||
|
||||
@@ -74,11 +74,13 @@ function hslToHex(h: number, s: number, l: number): string {
|
||||
gn = x;
|
||||
bn = c;
|
||||
} else if (h < 300) {
|
||||
rn = c;
|
||||
bn = x;
|
||||
} else {
|
||||
rn = x;
|
||||
gn = 0;
|
||||
bn = c;
|
||||
} else {
|
||||
rn = c;
|
||||
gn = 0;
|
||||
bn = x;
|
||||
}
|
||||
|
||||
const r = Math.round((rn + m) * 255);
|
||||
@@ -305,10 +307,11 @@ export function ColorPanel() {
|
||||
const [pickerTarget, setPickerTarget] = useState<ColorTarget | null>(null);
|
||||
const [hexInput, setHexInput] = useState(foregroundColor);
|
||||
|
||||
// Keep hex input in sync with store
|
||||
// Keep hex input in sync with the active color based on pickerTarget
|
||||
useEffect(() => {
|
||||
setHexInput(foregroundColor);
|
||||
}, [foregroundColor]);
|
||||
const color = pickerTarget === "bg" ? backgroundColor : foregroundColor;
|
||||
setHexInput(color);
|
||||
}, [foregroundColor, backgroundColor, pickerTarget]);
|
||||
|
||||
const activeColor = pickerTarget === "bg" ? backgroundColor : foregroundColor;
|
||||
const setActiveColor = pickerTarget === "bg" ? setBackgroundColor : setForegroundColor;
|
||||
@@ -333,16 +336,17 @@ export function ColorPanel() {
|
||||
setHexInput(v);
|
||||
if (!v.startsWith("#")) v = `#${v}`;
|
||||
if (isValidHex(v)) {
|
||||
setForegroundColor(v.toLowerCase());
|
||||
setActiveColor(v.toLowerCase());
|
||||
}
|
||||
},
|
||||
[setForegroundColor],
|
||||
[setActiveColor],
|
||||
);
|
||||
|
||||
const handleHexInputBlur = useCallback(() => {
|
||||
// Reset to current foreground if invalid
|
||||
setHexInput(foregroundColor);
|
||||
}, [foregroundColor]);
|
||||
// Reset to current active color if invalid
|
||||
const color = pickerTarget === "bg" ? backgroundColor : foregroundColor;
|
||||
setHexInput(color);
|
||||
}, [foregroundColor, backgroundColor, pickerTarget]);
|
||||
|
||||
return (
|
||||
<div className="p-3" data-testid="color-panel">
|
||||
|
||||
@@ -81,9 +81,6 @@ interface HistoryEntry {
|
||||
|
||||
export function HistoryPanel() {
|
||||
const lastAction = useEditorStore((s) => s.lastAction);
|
||||
const temporalStore = useEditorStore.temporal.getState();
|
||||
const pastStates = temporalStore.pastStates;
|
||||
const futureStates = temporalStore.futureStates;
|
||||
|
||||
// Force re-render when history changes by subscribing to history version
|
||||
useEditorStore((s) => s._historyVersion);
|
||||
@@ -154,10 +151,10 @@ export function HistoryPanel() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={undo}
|
||||
disabled={pastStates.length === 0}
|
||||
disabled={useEditorStore.temporal.getState().pastStates.length === 0}
|
||||
className={cn(
|
||||
"p-1 rounded transition-colors",
|
||||
pastStates.length > 0
|
||||
useEditorStore.temporal.getState().pastStates.length > 0
|
||||
? "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
: "text-muted-foreground/30 cursor-not-allowed",
|
||||
)}
|
||||
@@ -169,10 +166,10 @@ export function HistoryPanel() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={redo}
|
||||
disabled={futureStates.length === 0}
|
||||
disabled={useEditorStore.temporal.getState().futureStates.length === 0}
|
||||
className={cn(
|
||||
"p-1 rounded transition-colors",
|
||||
futureStates.length > 0
|
||||
useEditorStore.temporal.getState().futureStates.length > 0
|
||||
? "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
: "text-muted-foreground/30 cursor-not-allowed",
|
||||
)}
|
||||
@@ -181,7 +178,9 @@ export function HistoryPanel() {
|
||||
>
|
||||
<Redo2 size={14} />
|
||||
</button>
|
||||
<span className="ml-auto text-[10px] text-muted-foreground">{pastStates.length} / 50</span>
|
||||
<span className="ml-auto text-[10px] text-muted-foreground">
|
||||
{useEditorStore.temporal.getState().pastStates.length} / 50
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* History list */}
|
||||
|
||||
@@ -196,8 +196,8 @@ export function NavigatorPanel() {
|
||||
|
||||
const handleZoomSlider = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = Number.parseFloat(e.target.value);
|
||||
setZoom(val);
|
||||
const t = Number.parseFloat(e.target.value);
|
||||
setZoom(MIN_ZOOM * (MAX_ZOOM / MIN_ZOOM) ** t);
|
||||
},
|
||||
[setZoom],
|
||||
);
|
||||
@@ -252,10 +252,10 @@ export function NavigatorPanel() {
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min={MIN_ZOOM}
|
||||
max={MAX_ZOOM}
|
||||
step={0.01}
|
||||
value={zoom}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.001}
|
||||
value={Math.log(zoom / MIN_ZOOM) / Math.log(MAX_ZOOM / MIN_ZOOM)}
|
||||
onChange={handleZoomSlider}
|
||||
className={cn(
|
||||
"flex-1 h-1 appearance-none rounded-full bg-muted",
|
||||
|
||||
Reference in New Issue
Block a user