mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: split gridSize
from enabled state & support custom gridStep
(#8364)
This commit is contained in:
parent
4320a3cf41
commit
3cfcc7b489
31 changed files with 737 additions and 278 deletions
67
packages/excalidraw/components/Stats/CanvasGrid.tsx
Normal file
67
packages/excalidraw/components/Stats/CanvasGrid.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import StatsDragInput from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
import { getNormalizedGridStep } from "../../scene";
|
||||
|
||||
interface PositionProps {
|
||||
property: "gridStep";
|
||||
scene: Scene;
|
||||
appState: AppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
}
|
||||
|
||||
const STEP_SIZE = 5;
|
||||
|
||||
const CanvasGrid = ({
|
||||
property,
|
||||
scene,
|
||||
appState,
|
||||
setAppState,
|
||||
}: PositionProps) => {
|
||||
return (
|
||||
<StatsDragInput
|
||||
label="Grid step"
|
||||
sensitivity={8}
|
||||
elements={[]}
|
||||
dragInputCallback={({
|
||||
nextValue,
|
||||
instantChange,
|
||||
shouldChangeByStepSize,
|
||||
setInputValue,
|
||||
}) => {
|
||||
setAppState((state) => {
|
||||
let nextGridStep;
|
||||
|
||||
if (nextValue) {
|
||||
nextGridStep = nextValue;
|
||||
} else if (instantChange) {
|
||||
nextGridStep = shouldChangeByStepSize
|
||||
? getStepSizedValue(
|
||||
state.gridStep + STEP_SIZE * Math.sign(instantChange),
|
||||
STEP_SIZE,
|
||||
)
|
||||
: state.gridStep + instantChange;
|
||||
}
|
||||
|
||||
if (!nextGridStep) {
|
||||
setInputValue(state.gridStep);
|
||||
return null;
|
||||
}
|
||||
|
||||
nextGridStep = getNormalizedGridStep(nextGridStep);
|
||||
setInputValue(nextGridStep);
|
||||
return {
|
||||
gridStep: nextGridStep,
|
||||
};
|
||||
});
|
||||
}}
|
||||
scene={scene}
|
||||
value={appState.gridStep}
|
||||
property={property}
|
||||
appState={appState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CanvasGrid;
|
|
@ -18,7 +18,8 @@
|
|||
flex-shrink: 0;
|
||||
border: 1px solid var(--default-border-color);
|
||||
border-right: 0;
|
||||
width: 2rem;
|
||||
padding: 0 0.5rem 0 0.75rem;
|
||||
min-width: 1rem;
|
||||
height: 2rem;
|
||||
box-sizing: border-box;
|
||||
color: var(--popup-text-color);
|
||||
|
|
|
@ -29,6 +29,7 @@ export type DragInputCallbackType<
|
|||
nextValue?: number;
|
||||
property: P;
|
||||
originalAppState: AppState;
|
||||
setInputValue: (value: number) => void;
|
||||
}) => void;
|
||||
|
||||
interface StatsDragInputProps<
|
||||
|
@ -45,6 +46,8 @@ interface StatsDragInputProps<
|
|||
property: T;
|
||||
scene: Scene;
|
||||
appState: AppState;
|
||||
/** how many px you need to drag to get 1 unit change */
|
||||
sensitivity?: number;
|
||||
}
|
||||
|
||||
const StatsDragInput = <
|
||||
|
@ -61,6 +64,7 @@ const StatsDragInput = <
|
|||
property,
|
||||
scene,
|
||||
appState,
|
||||
sensitivity = 1,
|
||||
}: StatsDragInputProps<T, E>) => {
|
||||
const app = useApp();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
@ -126,6 +130,7 @@ const StatsDragInput = <
|
|||
nextValue: rounded,
|
||||
property,
|
||||
originalAppState: appState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
});
|
||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
||||
}
|
||||
|
@ -172,6 +177,8 @@ const StatsDragInput = <
|
|||
ref={labelRef}
|
||||
onPointerDown={(event) => {
|
||||
if (inputRef.current && editable) {
|
||||
document.body.classList.add("excalidraw-cursor-resize");
|
||||
|
||||
let startValue = Number(inputRef.current.value);
|
||||
if (isNaN(startValue)) {
|
||||
startValue = 0;
|
||||
|
@ -196,35 +203,43 @@ const StatsDragInput = <
|
|||
|
||||
const originalAppState: AppState = cloneJSON(appState);
|
||||
|
||||
let accumulatedChange: number | null = null;
|
||||
|
||||
document.body.classList.add("excalidraw-cursor-resize");
|
||||
let accumulatedChange = 0;
|
||||
let stepChange = 0;
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
if (!accumulatedChange) {
|
||||
accumulatedChange = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
lastPointer &&
|
||||
originalElementsMap !== null &&
|
||||
originalElements !== null &&
|
||||
accumulatedChange !== null
|
||||
originalElements !== null
|
||||
) {
|
||||
const instantChange = event.clientX - lastPointer.x;
|
||||
accumulatedChange += instantChange;
|
||||
|
||||
dragInputCallback({
|
||||
accumulatedChange,
|
||||
instantChange,
|
||||
originalElements,
|
||||
originalElementsMap,
|
||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||
shouldChangeByStepSize: event.shiftKey,
|
||||
property,
|
||||
scene,
|
||||
originalAppState,
|
||||
});
|
||||
if (instantChange !== 0) {
|
||||
stepChange += instantChange;
|
||||
|
||||
if (Math.abs(stepChange) >= sensitivity) {
|
||||
stepChange =
|
||||
Math.sign(stepChange) *
|
||||
Math.floor(Math.abs(stepChange) / sensitivity);
|
||||
|
||||
accumulatedChange += stepChange;
|
||||
|
||||
dragInputCallback({
|
||||
accumulatedChange,
|
||||
instantChange: stepChange,
|
||||
originalElements,
|
||||
originalElementsMap,
|
||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||
shouldChangeByStepSize: event.shiftKey,
|
||||
property,
|
||||
scene,
|
||||
originalAppState,
|
||||
setInputValue: (value) => setInputValue(String(value)),
|
||||
});
|
||||
|
||||
stepChange = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastPointer = {
|
||||
|
@ -246,7 +261,8 @@ const StatsDragInput = <
|
|||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
||||
|
||||
lastPointer = null;
|
||||
accumulatedChange = null;
|
||||
accumulatedChange = 0;
|
||||
stepChange = 0;
|
||||
originalElements = null;
|
||||
originalElementsMap = null;
|
||||
|
||||
|
|
|
@ -2,7 +2,11 @@ import { useEffect, useMemo, useState, memo } from "react";
|
|||
import { getCommonBounds } from "../../element/bounds";
|
||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||
import { t } from "../../i18n";
|
||||
import type { AppState, ExcalidrawProps } from "../../types";
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
} from "../../types";
|
||||
import { CloseIcon } from "../icons";
|
||||
import { Island } from "../Island";
|
||||
import { throttle } from "lodash";
|
||||
|
@ -16,17 +20,17 @@ import MultiFontSize from "./MultiFontSize";
|
|||
import Position from "./Position";
|
||||
import MultiPosition from "./MultiPosition";
|
||||
import Collapsible from "./Collapsible";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||
import { getAtomicUnits } from "./utils";
|
||||
import { STATS_PANELS } from "../../constants";
|
||||
import { isElbowArrow } from "../../element/typeChecks";
|
||||
import CanvasGrid from "./CanvasGrid";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./Stats.scss";
|
||||
|
||||
interface StatsProps {
|
||||
scene: Scene;
|
||||
app: AppClassProperties;
|
||||
onClose: () => void;
|
||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||
}
|
||||
|
@ -35,11 +39,13 @@ const STATS_TIMEOUT = 50;
|
|||
|
||||
export const Stats = (props: StatsProps) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const sceneNonce = props.scene.getSceneNonce() || 1;
|
||||
const selectedElements = props.scene.getSelectedElements({
|
||||
const sceneNonce = props.app.scene.getSceneNonce() || 1;
|
||||
const selectedElements = props.app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: false,
|
||||
});
|
||||
const gridModeEnabled =
|
||||
props.app.props.gridModeEnabled ?? appState.gridModeEnabled;
|
||||
|
||||
return (
|
||||
<StatsInner
|
||||
|
@ -47,6 +53,7 @@ export const Stats = (props: StatsProps) => {
|
|||
appState={appState}
|
||||
sceneNonce={sceneNonce}
|
||||
selectedElements={selectedElements}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -97,17 +104,20 @@ Stats.StatsRows = StatsRows;
|
|||
|
||||
export const StatsInner = memo(
|
||||
({
|
||||
scene,
|
||||
app,
|
||||
onClose,
|
||||
renderCustomStats,
|
||||
selectedElements,
|
||||
appState,
|
||||
sceneNonce,
|
||||
gridModeEnabled,
|
||||
}: StatsProps & {
|
||||
sceneNonce: number;
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||
appState: AppState;
|
||||
gridModeEnabled: boolean;
|
||||
}) => {
|
||||
const scene = app.scene;
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
|
@ -189,6 +199,19 @@ export const StatsInner = memo(
|
|||
<div>{t("stats.height")}</div>
|
||||
<div>{sceneDimension.height}</div>
|
||||
</StatsRow>
|
||||
{gridModeEnabled && (
|
||||
<>
|
||||
<StatsRow heading>Canvas</StatsRow>
|
||||
<StatsRow>
|
||||
<CanvasGrid
|
||||
property="gridStep"
|
||||
scene={scene}
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
</StatsRow>
|
||||
</>
|
||||
)}
|
||||
</StatsRows>
|
||||
|
||||
{renderCustomStats?.(elements, appState)}
|
||||
|
@ -362,7 +385,9 @@ export const StatsInner = memo(
|
|||
return (
|
||||
prev.sceneNonce === next.sceneNonce &&
|
||||
prev.selectedElements === next.selectedElements &&
|
||||
prev.appState.stats.panels === next.appState.stats.panels
|
||||
prev.appState.stats.panels === next.appState.stats.panels &&
|
||||
prev.gridModeEnabled === next.gridModeEnabled &&
|
||||
prev.appState.gridStep === next.appState.gridStep
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -41,7 +41,8 @@ export type StatsInputProperty =
|
|||
| "width"
|
||||
| "height"
|
||||
| "angle"
|
||||
| "fontSize";
|
||||
| "fontSize"
|
||||
| "gridStep";
|
||||
|
||||
export const SMALLEST_DELTA = 0.01;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue