mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
Removed disabled ESLint rules by refactoring state management to follow React best practices. Changes include: - Replaced setState-in-effect pattern with state-during-render pattern for viewport resets - Changed from ref-based to state-based storage for initialScale to avoid ref updates during render - Implemented React-recommended pattern for deriving state from props (similar to getDerivedStateFromProps) - Properly track pattern changes in state to detect when viewport should reset This eliminates the need for ESLint disable comments while maintaining the same functionality. The viewport now correctly resets when patterns change, using a pattern that React explicitly recommends for this use case. Fixes #30 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
/**
|
|
* useCanvasViewport Hook
|
|
*
|
|
* Manages canvas viewport state including zoom, pan, and container size
|
|
* Handles wheel zoom and button zoom operations
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback, type RefObject } from "react";
|
|
import type Konva from "konva";
|
|
import type { PesPatternData } from "../formats/import/pesImporter";
|
|
import type { MachineInfo } from "../types/machine";
|
|
import { calculateInitialScale } from "../utils/konvaRenderers";
|
|
import { calculateZoomToPoint } from "../components/PatternCanvas/patternCanvasHelpers";
|
|
|
|
interface UseCanvasViewportOptions {
|
|
containerRef: RefObject<HTMLDivElement | null>;
|
|
pesData: PesPatternData | null;
|
|
uploadedPesData: PesPatternData | null;
|
|
machineInfo: MachineInfo | null;
|
|
}
|
|
|
|
export function useCanvasViewport({
|
|
containerRef,
|
|
pesData,
|
|
uploadedPesData,
|
|
machineInfo,
|
|
}: UseCanvasViewportOptions) {
|
|
const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
|
|
const [stageScale, setStageScale] = useState(1);
|
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
const [initialScale, setInitialScale] = useState(1);
|
|
|
|
// Track the last processed pattern to detect changes during render
|
|
const [lastProcessedPattern, setLastProcessedPattern] =
|
|
useState<PesPatternData | null>(null);
|
|
|
|
// Track container size with ResizeObserver
|
|
useEffect(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
const updateSize = () => {
|
|
if (containerRef.current) {
|
|
const width = containerRef.current.clientWidth;
|
|
const height = containerRef.current.clientHeight;
|
|
setContainerSize({ width, height });
|
|
}
|
|
};
|
|
|
|
// Initial size
|
|
updateSize();
|
|
|
|
// Watch for resize
|
|
const resizeObserver = new ResizeObserver(updateSize);
|
|
resizeObserver.observe(containerRef.current);
|
|
|
|
return () => resizeObserver.disconnect();
|
|
}, [containerRef]);
|
|
|
|
// Reset viewport when pattern changes (during render, not in effect)
|
|
// This follows the React-recommended pattern for deriving state from props
|
|
const currentPattern = uploadedPesData || pesData;
|
|
if (
|
|
currentPattern &&
|
|
currentPattern !== lastProcessedPattern &&
|
|
containerSize.width > 0
|
|
) {
|
|
const { bounds } = currentPattern;
|
|
const viewWidth = machineInfo
|
|
? machineInfo.maxWidth
|
|
: bounds.maxX - bounds.minX;
|
|
const viewHeight = machineInfo
|
|
? machineInfo.maxHeight
|
|
: bounds.maxY - bounds.minY;
|
|
|
|
const newInitialScale = calculateInitialScale(
|
|
containerSize.width,
|
|
containerSize.height,
|
|
viewWidth,
|
|
viewHeight,
|
|
);
|
|
|
|
// Update state during render when pattern changes
|
|
// This is the recommended React pattern for resetting state based on props
|
|
setLastProcessedPattern(currentPattern);
|
|
setInitialScale(newInitialScale);
|
|
setStageScale(newInitialScale);
|
|
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
|
}
|
|
|
|
// Wheel zoom handler
|
|
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
|
|
e.evt.preventDefault();
|
|
|
|
const stage = e.target.getStage();
|
|
if (!stage) return;
|
|
|
|
const pointer = stage.getPointerPosition();
|
|
if (!pointer) return;
|
|
|
|
const scaleBy = 1.1;
|
|
const direction = e.evt.deltaY > 0 ? -1 : 1;
|
|
|
|
setStageScale((oldScale) => {
|
|
const newScale = Math.max(
|
|
0.1,
|
|
Math.min(direction > 0 ? oldScale * scaleBy : oldScale / scaleBy, 2),
|
|
);
|
|
|
|
// Zoom towards pointer
|
|
setStagePos((prevPos) =>
|
|
calculateZoomToPoint(oldScale, newScale, pointer, prevPos),
|
|
);
|
|
|
|
return newScale;
|
|
});
|
|
}, []);
|
|
|
|
// Zoom control handlers
|
|
const handleZoomIn = useCallback(() => {
|
|
setStageScale((oldScale) => {
|
|
const newScale = Math.max(0.1, Math.min(oldScale * 1.2, 2));
|
|
|
|
// Zoom towards center of viewport
|
|
const center = {
|
|
x: containerSize.width / 2,
|
|
y: containerSize.height / 2,
|
|
};
|
|
setStagePos((prevPos) =>
|
|
calculateZoomToPoint(oldScale, newScale, center, prevPos),
|
|
);
|
|
|
|
return newScale;
|
|
});
|
|
}, [containerSize]);
|
|
|
|
const handleZoomOut = useCallback(() => {
|
|
setStageScale((oldScale) => {
|
|
const newScale = Math.max(0.1, Math.min(oldScale / 1.2, 2));
|
|
|
|
// Zoom towards center of viewport
|
|
const center = {
|
|
x: containerSize.width / 2,
|
|
y: containerSize.height / 2,
|
|
};
|
|
setStagePos((prevPos) =>
|
|
calculateZoomToPoint(oldScale, newScale, center, prevPos),
|
|
);
|
|
|
|
return newScale;
|
|
});
|
|
}, [containerSize]);
|
|
|
|
const handleZoomReset = useCallback(() => {
|
|
setStageScale(initialScale);
|
|
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
|
}, [initialScale, containerSize]);
|
|
|
|
return {
|
|
// State
|
|
stagePos,
|
|
stageScale,
|
|
containerSize,
|
|
|
|
// Handlers
|
|
handleWheel,
|
|
handleZoomIn,
|
|
handleZoomOut,
|
|
handleZoomReset,
|
|
};
|
|
}
|