mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 02:13:41 +00:00
Compare commits
3 commits
212d21e065
...
3ba87ba192
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba87ba192 | ||
|
|
9d30eae901 | ||
|
|
93ebc8398c |
5 changed files with 116 additions and 38 deletions
|
|
@ -46,7 +46,7 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
||||||
const gridColor = canvasColors.grid();
|
const gridColor = canvasColors.grid();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="grid">
|
<Group name="grid" listening={false}>
|
||||||
{lines.verticalLines.map((points, i) => (
|
{lines.verticalLines.map((points, i) => (
|
||||||
<Line
|
<Line
|
||||||
key={`v-${i}`}
|
key={`v-${i}`}
|
||||||
|
|
@ -73,7 +73,7 @@ export const Origin = memo(() => {
|
||||||
const originColor = canvasColors.origin();
|
const originColor = canvasColors.origin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="origin">
|
<Group name="origin" listening={false}>
|
||||||
<Line points={[-10, 0, 10, 0]} stroke={originColor} strokeWidth={2} />
|
<Line points={[-10, 0, 10, 0]} stroke={originColor} strokeWidth={2} />
|
||||||
<Line points={[0, -10, 0, 10]} stroke={originColor} strokeWidth={2} />
|
<Line points={[0, -10, 0, 10]} stroke={originColor} strokeWidth={2} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
@ -93,7 +93,7 @@ export const Hoop = memo(({ machineInfo }: HoopProps) => {
|
||||||
const hoopColor = canvasColors.hoop();
|
const hoopColor = canvasColors.hoop();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="hoop">
|
<Group name="hoop" listening={false}>
|
||||||
<Rect
|
<Rect
|
||||||
x={hoopLeft}
|
x={hoopLeft}
|
||||||
y={hoopTop}
|
y={hoopTop}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ export function PatternCanvas() {
|
||||||
handleZoomIn,
|
handleZoomIn,
|
||||||
handleZoomOut,
|
handleZoomOut,
|
||||||
handleZoomReset,
|
handleZoomReset,
|
||||||
|
handleStageDragStart,
|
||||||
|
handleStageDragEnd,
|
||||||
} = useCanvasViewport({
|
} = useCanvasViewport({
|
||||||
containerRef,
|
containerRef,
|
||||||
pesData,
|
pesData,
|
||||||
|
|
@ -165,16 +167,8 @@ export function PatternCanvas() {
|
||||||
scaleY={stageScale}
|
scaleY={stageScale}
|
||||||
draggable
|
draggable
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
onDragStart={() => {
|
onDragStart={handleStageDragStart}
|
||||||
if (stageRef.current) {
|
onDragEnd={handleStageDragEnd}
|
||||||
stageRef.current.container().style.cursor = "grabbing";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onDragEnd={() => {
|
|
||||||
if (stageRef.current) {
|
|
||||||
stageRef.current.container().style.cursor = "grab";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
stageRef.current = node;
|
stageRef.current = node;
|
||||||
if (node) {
|
if (node) {
|
||||||
|
|
@ -182,8 +176,8 @@ export function PatternCanvas() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Background layer: grid, origin, hoop */}
|
{/* Background layer: grid, origin, hoop - static, no event listening */}
|
||||||
<Layer>
|
<Layer listening={false}>
|
||||||
{displayPattern && (
|
{displayPattern && (
|
||||||
<>
|
<>
|
||||||
<Grid
|
<Grid
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,16 @@
|
||||||
* useCanvasViewport Hook
|
* useCanvasViewport Hook
|
||||||
*
|
*
|
||||||
* Manages canvas viewport state including zoom, pan, and container size
|
* Manages canvas viewport state including zoom, pan, and container size
|
||||||
* Handles wheel zoom and button zoom operations
|
* Handles wheel zoom, button zoom operations, and stage drag cursor updates
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, type RefObject } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
type RefObject,
|
||||||
|
} from "react";
|
||||||
import type Konva from "konva";
|
import type Konva from "konva";
|
||||||
import type { PesPatternData } from "../../formats/import/pesImporter";
|
import type { PesPatternData } from "../../formats/import/pesImporter";
|
||||||
import type { MachineInfo } from "../../types/machine";
|
import type { MachineInfo } from "../../types/machine";
|
||||||
|
|
@ -87,7 +93,12 @@ export function useCanvasViewport({
|
||||||
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wheel zoom handler
|
// Wheel zoom handler with RAF throttling and delta accumulation
|
||||||
|
const wheelThrottleRef = useRef<number | null>(null);
|
||||||
|
const accumulatedDeltaRef = useRef<number>(0);
|
||||||
|
const lastPointerRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
const lastStageRef = useRef<Konva.Stage | null>(null);
|
||||||
|
|
||||||
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
|
const handleWheel = useCallback((e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
e.evt.preventDefault();
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
|
@ -97,24 +108,60 @@ export function useCanvasViewport({
|
||||||
const pointer = stage.getPointerPosition();
|
const pointer = stage.getPointerPosition();
|
||||||
if (!pointer) return;
|
if (!pointer) return;
|
||||||
|
|
||||||
const scaleBy = 1.1;
|
// Accumulate deltaY from all events during throttle period
|
||||||
const direction = e.evt.deltaY > 0 ? -1 : 1;
|
accumulatedDeltaRef.current += e.evt.deltaY;
|
||||||
|
lastPointerRef.current = pointer;
|
||||||
|
lastStageRef.current = stage;
|
||||||
|
|
||||||
setStageScale((oldScale) => {
|
// Skip if throttle already in progress
|
||||||
const newScale = Math.max(
|
if (wheelThrottleRef.current !== null) {
|
||||||
0.1,
|
return;
|
||||||
Math.min(direction > 0 ? oldScale * scaleBy : oldScale / scaleBy, 2),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// Zoom towards pointer
|
// Schedule update on next animation frame (~16ms)
|
||||||
setStagePos((prevPos) =>
|
wheelThrottleRef.current = requestAnimationFrame(() => {
|
||||||
calculateZoomToPoint(oldScale, newScale, pointer, prevPos),
|
const accumulatedDelta = accumulatedDeltaRef.current;
|
||||||
);
|
const pointer = lastPointerRef.current;
|
||||||
|
const stage = lastStageRef.current;
|
||||||
|
|
||||||
return newScale;
|
if (!pointer || !stage || accumulatedDelta === 0) {
|
||||||
|
wheelThrottleRef.current = null;
|
||||||
|
accumulatedDeltaRef.current = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleBy = 1.1;
|
||||||
|
const direction = accumulatedDelta > 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset accumulator and throttle
|
||||||
|
wheelThrottleRef.current = null;
|
||||||
|
accumulatedDeltaRef.current = 0;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup wheel throttle on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (wheelThrottleRef.current !== null) {
|
||||||
|
cancelAnimationFrame(wheelThrottleRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Zoom control handlers
|
// Zoom control handlers
|
||||||
const handleZoomIn = useCallback(() => {
|
const handleZoomIn = useCallback(() => {
|
||||||
setStageScale((oldScale) => {
|
setStageScale((oldScale) => {
|
||||||
|
|
@ -155,6 +202,27 @@ export function useCanvasViewport({
|
||||||
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
setStagePos({ x: containerSize.width / 2, y: containerSize.height / 2 });
|
||||||
}, [initialScale, containerSize]);
|
}, [initialScale, containerSize]);
|
||||||
|
|
||||||
|
// Stage drag handlers - cursor updates immediately for better UX
|
||||||
|
const handleStageDragStart = useCallback(
|
||||||
|
(e: Konva.KonvaEventObject<DragEvent>) => {
|
||||||
|
const stage = e.target.getStage();
|
||||||
|
if (stage) {
|
||||||
|
stage.container().style.cursor = "grabbing";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStageDragEnd = useCallback(
|
||||||
|
(e: Konva.KonvaEventObject<DragEvent>) => {
|
||||||
|
const stage = e.target.getStage();
|
||||||
|
if (stage) {
|
||||||
|
stage.container().style.cursor = "grab";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
stagePos,
|
stagePos,
|
||||||
|
|
@ -166,5 +234,7 @@ export function useCanvasViewport({
|
||||||
handleZoomIn,
|
handleZoomIn,
|
||||||
handleZoomOut,
|
handleZoomOut,
|
||||||
handleZoomReset,
|
handleZoomReset,
|
||||||
|
handleStageDragStart,
|
||||||
|
handleStageDragEnd,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,8 @@ export function usePatternTransform({
|
||||||
setPatternOffset(centerOffset.x, centerOffset.y);
|
setPatternOffset(centerOffset.x, centerOffset.y);
|
||||||
}, [pesData, setPatternOffset]);
|
}, [pesData, setPatternOffset]);
|
||||||
|
|
||||||
// Pattern drag handlers
|
// Pattern drag handler - only updates state when drag is complete
|
||||||
|
// Konva handles the visual drag internally, no need to update React state during drag
|
||||||
const handlePatternDragEnd = useCallback(
|
const handlePatternDragEnd = useCallback(
|
||||||
(e: Konva.KonvaEventObject<DragEvent>) => {
|
(e: Konva.KonvaEventObject<DragEvent>) => {
|
||||||
const newOffset = {
|
const newOffset = {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import { onPatternDeleted } from "./storeEvents";
|
||||||
import { calculatePatternCenter } from "../components/PatternCanvas/patternCanvasHelpers";
|
import { calculatePatternCenter } from "../components/PatternCanvas/patternCanvasHelpers";
|
||||||
import { calculateRotatedBounds } from "../utils/rotationUtils";
|
import { calculateRotatedBounds } from "../utils/rotationUtils";
|
||||||
|
|
||||||
|
// Conditional logging for development only
|
||||||
|
const isDev = import.meta.env.DEV;
|
||||||
|
|
||||||
interface PatternState {
|
interface PatternState {
|
||||||
// Original pattern (pre-upload)
|
// Original pattern (pre-upload)
|
||||||
pesData: PesPatternData | null;
|
pesData: PesPatternData | null;
|
||||||
|
|
@ -79,13 +82,17 @@ export const usePatternStore = create<PatternState>((set) => ({
|
||||||
// Update pattern offset (for original pattern only)
|
// Update pattern offset (for original pattern only)
|
||||||
setPatternOffset: (x: number, y: number) => {
|
setPatternOffset: (x: number, y: number) => {
|
||||||
set({ patternOffset: { x, y } });
|
set({ patternOffset: { x, y } });
|
||||||
console.log("[PatternStore] Pattern offset changed:", { x, y });
|
if (isDev) {
|
||||||
|
console.log("[PatternStore] Pattern offset changed:", { x, y });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set pattern rotation (for original pattern only)
|
// Set pattern rotation (for original pattern only)
|
||||||
setPatternRotation: (rotation: number) => {
|
setPatternRotation: (rotation: number) => {
|
||||||
set({ patternRotation: rotation % 360 });
|
set({ patternRotation: rotation % 360 });
|
||||||
console.log("[PatternStore] Pattern rotation changed:", rotation);
|
if (isDev) {
|
||||||
|
console.log("[PatternStore] Pattern rotation changed:", rotation);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set uploaded pattern data (called after upload completes)
|
// Set uploaded pattern data (called after upload completes)
|
||||||
|
|
@ -101,13 +108,17 @@ export const usePatternStore = create<PatternState>((set) => ({
|
||||||
// Optionally set filename if provided (for resume/reconnect scenarios)
|
// Optionally set filename if provided (for resume/reconnect scenarios)
|
||||||
...(fileName && { currentFileName: fileName }),
|
...(fileName && { currentFileName: fileName }),
|
||||||
});
|
});
|
||||||
console.log("[PatternStore] Uploaded pattern set");
|
if (isDev) {
|
||||||
|
console.log("[PatternStore] Uploaded pattern set");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Clear uploaded pattern (called when deleting from machine)
|
// Clear uploaded pattern (called when deleting from machine)
|
||||||
// This reverts to pre-upload state, keeping pesData so user can re-adjust and re-upload
|
// This reverts to pre-upload state, keeping pesData so user can re-adjust and re-upload
|
||||||
clearUploadedPattern: () => {
|
clearUploadedPattern: () => {
|
||||||
console.log("[PatternStore] CLEARING uploaded pattern...");
|
if (isDev) {
|
||||||
|
console.log("[PatternStore] CLEARING uploaded pattern...");
|
||||||
|
}
|
||||||
set({
|
set({
|
||||||
uploadedPesData: null,
|
uploadedPesData: null,
|
||||||
uploadedPatternOffset: { x: 0, y: 0 },
|
uploadedPatternOffset: { x: 0, y: 0 },
|
||||||
|
|
@ -115,9 +126,11 @@ export const usePatternStore = create<PatternState>((set) => ({
|
||||||
// Keep pesData, currentFileName, patternOffset, patternRotation
|
// Keep pesData, currentFileName, patternOffset, patternRotation
|
||||||
// so user can adjust and re-upload
|
// so user can adjust and re-upload
|
||||||
});
|
});
|
||||||
console.log(
|
if (isDev) {
|
||||||
"[PatternStore] Uploaded pattern cleared - back to editable mode",
|
console.log(
|
||||||
);
|
"[PatternStore] Uploaded pattern cleared - back to editable mode",
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Reset pattern offset to default
|
// Reset pattern offset to default
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue