Fixes #68 added zoom to pan, removed the weird implementation from before

This commit is contained in:
stan-smith
2025-10-03 17:34:14 +01:00
parent 4e13033609
commit d3fdfea8a5
9 changed files with 152 additions and 6 deletions

View File

@@ -14,6 +14,7 @@ import { Close as CloseIcon } from '@mui/icons-material';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { HotkeySettings } from '../HotkeySettings/HotkeySettings';
import { PanSettings } from '../PanSettings/PanSettings';
import { ZoomSettings } from '../ZoomSettings/ZoomSettings';
import { ConnectorSettings } from '../ConnectorSettings/ConnectorSettings';
export const SettingsDialog = () => {
@@ -57,13 +58,15 @@ export const SettingsDialog = () => {
<Tabs value={tabValue} onChange={handleTabChange} sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tab label="Hotkeys" />
<Tab label="Pan Controls" />
<Tab label="Zoom" />
<Tab label="Connectors" />
</Tabs>
<Box sx={{ mt: 2 }}>
{tabValue === 0 && <HotkeySettings />}
{tabValue === 1 && <PanSettings />}
{tabValue === 2 && <ConnectorSettings />}
{tabValue === 2 && <ZoomSettings />}
{tabValue === 3 && <ConnectorSettings />}
</Box>
</DialogContent>
<DialogActions>

View File

@@ -0,0 +1,55 @@
import React from 'react';
import {
Box,
FormControl,
FormGroup,
FormControlLabel,
Switch,
Typography
} from '@mui/material';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { useLocale } from 'src/stores/localeStore';
export const ZoomSettings = () => {
const zoomSettings = useUiStateStore((state) => state.zoomSettings);
const setZoomSettings = useUiStateStore((state) => state.actions.setZoomSettings);
const locale = useLocale();
const handleToggle = (setting: keyof typeof zoomSettings) => {
setZoomSettings({
...zoomSettings,
[setting]: !zoomSettings[setting]
});
};
return (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{locale.settings.zoom.description}
</Typography>
<FormControl component="fieldset" variant="standard">
<FormGroup>
<FormControlLabel
control={
<Switch
checked={zoomSettings.zoomToCursor}
onChange={() => handleToggle('zoomToCursor')}
/>
}
label={
<Box>
<Typography variant="body1">
{locale.settings.zoom.zoomToCursor}
</Typography>
<Typography variant="caption" color="text.secondary">
{locale.settings.zoom.zoomToCursorDesc}
</Typography>
</Box>
}
/>
</FormGroup>
</FormControl>
</Box>
);
};

View File

@@ -0,0 +1,9 @@
export interface ZoomSettings {
// Zoom behavior
zoomToCursor: boolean;
}
export const DEFAULT_ZOOM_SETTINGS: ZoomSettings = {
// Default to zoom-to-cursor for better UX
zoomToCursor: true
};

View File

@@ -110,6 +110,13 @@ const locale: LocaleProps = {
instructionAnd: "and",
instructionDrag: "drag",
instructionEnd: "to change it!"
},
settings: {
zoom: {
description: "Configure zoom behavior when using the mouse wheel.",
zoomToCursor: "Zoom to Cursor",
zoomToCursorDesc: "When enabled, zoom in/out centered on the mouse cursor position. When disabled, zoom is centered on the canvas."
}
}
};

View File

@@ -110,6 +110,13 @@ const locale: LocaleProps = {
instructionAnd: "并",
instructionDrag: "拖拽",
instructionEnd: "即可更改!"
},
settings: {
zoom: {
description: "配置使用鼠标滚轮时的缩放行为。",
zoomToCursor: "光标缩放",
zoomToCursorDesc: "启用时,以鼠标光标位置为中心进行缩放。禁用时,以画布中心进行缩放。"
}
}
};

View File

@@ -3,7 +3,7 @@ import { useModelStore } from 'src/stores/modelStore';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { ModeActions, State, SlimMouseEvent } from 'src/types';
import { DialogTypeEnum } from 'src/types/ui';
import { getMouse, getItemAtTile, generateId } from 'src/utils';
import { getMouse, getItemAtTile, generateId, incrementZoom, decrementZoom } from 'src/utils';
import { useResizeObserver } from 'src/hooks/useResizeObserver';
import { useScene } from 'src/hooks/useScene';
import { useHistory } from 'src/hooks/useHistory';
@@ -315,10 +315,56 @@ export const useInteractionManager = () => {
};
const onScroll = (e: WheelEvent) => {
const zoomToCursor = uiState.zoomSettings.zoomToCursor;
const oldZoom = uiState.zoom;
// Calculate new zoom level
let newZoom: number;
if (e.deltaY > 0) {
uiState.actions.decrementZoom();
newZoom = decrementZoom(oldZoom);
} else {
uiState.actions.incrementZoom();
newZoom = incrementZoom(oldZoom);
}
// If zoom didn't change (at min/max), no need to adjust scroll
if (newZoom === oldZoom) {
return;
}
if (zoomToCursor && rendererRef.current && rendererSize) {
// Get mouse position relative to the renderer viewport
const rect = rendererRef.current.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Calculate mouse position relative to viewport center
const mouseRelativeToCenterX = mouseX - rendererSize.width / 2;
const mouseRelativeToCenterY = mouseY - rendererSize.height / 2;
// The point under the cursor in world space (before zoom)
// World coordinates = (screen coordinates - scroll offset) / zoom
const worldX = (mouseRelativeToCenterX - uiState.scroll.position.x) / oldZoom;
const worldY = (mouseRelativeToCenterY - uiState.scroll.position.y) / oldZoom;
// After zooming, to keep the same world point under the cursor:
// screen coordinates = world coordinates * newZoom + scroll offset
// We want: mouseRelativeToCenterX = worldX * newZoom + newScrollX
// Therefore: newScrollX = mouseRelativeToCenterX - worldX * newZoom
const newScrollX = mouseRelativeToCenterX - worldX * newZoom;
const newScrollY = mouseRelativeToCenterY - worldY * newZoom;
// Apply zoom and adjusted scroll together
uiState.actions.setZoom(newZoom);
uiState.actions.setScroll({
position: {
x: newScrollX,
y: newScrollY
},
offset: uiState.scroll.offset
});
} else {
// Original behavior: zoom to center
uiState.actions.setZoom(newZoom);
}
};
@@ -347,7 +393,11 @@ export const useInteractionManager = () => {
uiState.mode.type,
onContextMenu,
uiState.actions,
uiState.rendererEl
uiState.rendererEl,
uiState.zoom,
uiState.scroll,
uiState.zoomSettings,
rendererSize
]);
const setInteractionsElement = useCallback((element: HTMLElement) => {

View File

@@ -10,6 +10,7 @@ import { UiStateStore } from 'src/types';
import { INITIAL_UI_STATE } from 'src/config';
import { DEFAULT_HOTKEY_PROFILE } from 'src/config/hotkeys';
import { DEFAULT_PAN_SETTINGS } from 'src/config/panSettings';
import { DEFAULT_ZOOM_SETTINGS } from 'src/config/zoomSettings';
const initialState = () => {
return createStore<UiStateStore>((set, get) => {
@@ -34,6 +35,7 @@ const initialState = () => {
enableDebugTools: false,
hotkeyProfile: DEFAULT_HOTKEY_PROFILE,
panSettings: DEFAULT_PAN_SETTINGS,
zoomSettings: DEFAULT_ZOOM_SETTINGS,
connectorInteractionMode: 'click', // Default to click mode
actions: {
@@ -104,6 +106,9 @@ const initialState = () => {
setPanSettings: (panSettings) => {
set({ panSettings });
},
setZoomSettings: (zoomSettings) => {
set({ zoomSettings });
},
setConnectorInteractionMode: (connectorInteractionMode) => {
set({ connectorInteractionMode });
}

View File

@@ -118,6 +118,13 @@ export interface LocaleProps {
instructionDrag: string;
instructionEnd: string;
};
settings: {
zoom: {
description: string;
zoomToCursor: string;
zoomToCursorDesc: string;
};
};
// other namespaces can be added here
}

View File

@@ -3,6 +3,7 @@ import { Icon } from './model';
import { ItemReference } from './scene';
import { HotkeyProfile } from 'src/config/hotkeys';
import { PanSettings } from 'src/config/panSettings';
import { ZoomSettings } from 'src/config/zoomSettings';
interface AddItemControls {
type: 'ADD_ITEM';
@@ -177,6 +178,7 @@ export interface UiState {
enableDebugTools: boolean;
hotkeyProfile: HotkeyProfile;
panSettings: PanSettings;
zoomSettings: ZoomSettings;
connectorInteractionMode: ConnectorInteractionMode;
}
@@ -201,6 +203,7 @@ export interface UiStateActions {
setEnableDebugTools: (enabled: boolean) => void;
setHotkeyProfile: (profile: HotkeyProfile) => void;
setPanSettings: (settings: PanSettings) => void;
setZoomSettings: (settings: ZoomSettings) => void;
setConnectorInteractionMode: (mode: ConnectorInteractionMode) => void;
}