mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: stats state leaking & race conds (#8177)
This commit is contained in:
parent
6ba9bd60e8
commit
744b3e5d09
14 changed files with 945 additions and 761 deletions
|
@ -1,37 +1,46 @@
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
import { isArrowElement } from "../../element/typeChecks";
|
import { isArrowElement } from "../../element/typeChecks";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
import { degreeToRadian, radianToDegree } from "../../math";
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
import type Scene from "../../scene/Scene";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface AngleProps {
|
interface AngleProps {
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
elementsMap: ElementsMap;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
|
property: "angle";
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 15;
|
const STEP_SIZE = 15;
|
||||||
|
|
||||||
const Angle = ({ element, elementsMap }: AngleProps) => {
|
const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
const handleDegreeChange: DragInputCallbackType = ({
|
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
scene,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement) {
|
if (origElement) {
|
||||||
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
if (!latestElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextAngle = degreeToRadian(nextValue);
|
const nextAngle = degreeToRadian(nextValue);
|
||||||
mutateElement(element, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(element)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,17 +60,18 @@ const Angle = ({ element, elementsMap }: AngleProps) => {
|
||||||
|
|
||||||
const nextAngle = degreeToRadian(nextAngleInDegrees);
|
const nextAngle = degreeToRadian(nextAngleInDegrees);
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(element)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Angle = ({ element, scene, appState, property }: AngleProps) => {
|
||||||
return (
|
return (
|
||||||
<DragInput
|
<DragInput
|
||||||
label="A"
|
label="A"
|
||||||
|
@ -70,6 +80,9 @@ const Angle = ({ element, elementsMap }: AngleProps) => {
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handleDegreeChange}
|
dragInputCallback={handleDegreeChange}
|
||||||
editable={isPropertyEditable(element, "angle")}
|
editable={isPropertyEditable(element, "angle")}
|
||||||
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
|
property={property}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, resizeElement } from "./utils";
|
import { getStepSizedValue, isPropertyEditable, resizeElement } from "./utils";
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||||
|
import type Scene from "../../scene/Scene";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface DimensionDragInputProps {
|
interface DimensionDragInputProps {
|
||||||
property: "width" | "height";
|
property: "width" | "height";
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
elementsMap: ElementsMap;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
const STEP_SIZE = 10;
|
||||||
|
@ -15,23 +18,23 @@ const _shouldKeepAspectRatio = (element: ExcalidrawElement) => {
|
||||||
return element.type === "image";
|
return element.type === "image";
|
||||||
};
|
};
|
||||||
|
|
||||||
const DimensionDragInput = ({
|
const handleDimensionChange: DragInputCallbackType<
|
||||||
property,
|
DimensionDragInputProps["property"]
|
||||||
element,
|
> = ({
|
||||||
elementsMap,
|
|
||||||
}: DimensionDragInputProps) => {
|
|
||||||
const handleDimensionChange: DragInputCallbackType = ({
|
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
shouldKeepAspectRatio,
|
shouldKeepAspectRatio,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
property,
|
||||||
|
scene,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement) {
|
if (origElement) {
|
||||||
const keepAspectRatio =
|
const keepAspectRatio =
|
||||||
shouldKeepAspectRatio || _shouldKeepAspectRatio(element);
|
shouldKeepAspectRatio || _shouldKeepAspectRatio(origElement);
|
||||||
const aspectRatio = origElement.width / origElement.height;
|
const aspectRatio = origElement.width / origElement.height;
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
|
@ -56,10 +59,8 @@ const DimensionDragInput = ({
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
keepAspectRatio,
|
keepAspectRatio,
|
||||||
element,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -100,14 +101,18 @@ const DimensionDragInput = ({
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
keepAspectRatio,
|
keepAspectRatio,
|
||||||
element,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DimensionDragInput = ({
|
||||||
|
property,
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
}: DimensionDragInputProps) => {
|
||||||
const value =
|
const value =
|
||||||
Math.round((property === "width" ? element.width : element.height) * 100) /
|
Math.round((property === "width" ? element.width : element.height) * 100) /
|
||||||
100;
|
100;
|
||||||
|
@ -119,6 +124,9 @@ const DimensionDragInput = ({
|
||||||
dragInputCallback={handleDimensionChange}
|
dragInputCallback={handleDimensionChange}
|
||||||
value={value}
|
value={value}
|
||||||
editable={isPropertyEditable(element, property)}
|
editable={isPropertyEditable(element, property)}
|
||||||
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
|
property={property}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,23 +3,19 @@ import { EVENT } from "../../constants";
|
||||||
import { KEYS } from "../../keys";
|
import { KEYS } from "../../keys";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
||||||
import { deepCopyElement } from "../../element/newElement";
|
import { deepCopyElement } from "../../element/newElement";
|
||||||
|
|
||||||
import "./DragInput.scss";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useApp } from "../App";
|
import { useApp } from "../App";
|
||||||
import { InlineIcon } from "../InlineIcon";
|
import { InlineIcon } from "../InlineIcon";
|
||||||
|
import type { StatsInputProperty } from "./utils";
|
||||||
import { SMALLEST_DELTA } from "./utils";
|
import { SMALLEST_DELTA } from "./utils";
|
||||||
import { StoreAction } from "../../store";
|
import { StoreAction } from "../../store";
|
||||||
|
import type Scene from "../../scene/Scene";
|
||||||
|
|
||||||
export type DragInputCallbackType = ({
|
import "./DragInput.scss";
|
||||||
accumulatedChange,
|
import type { AppState } from "../../types";
|
||||||
instantChange,
|
import { cloneJSON } from "../../utils";
|
||||||
originalElements,
|
|
||||||
originalElementsMap,
|
export type DragInputCallbackType<T extends StatsInputProperty> = (props: {
|
||||||
shouldKeepAspectRatio,
|
|
||||||
shouldChangeByStepSize,
|
|
||||||
nextValue,
|
|
||||||
}: {
|
|
||||||
accumulatedChange: number;
|
accumulatedChange: number;
|
||||||
instantChange: number;
|
instantChange: number;
|
||||||
originalElements: readonly ExcalidrawElement[];
|
originalElements: readonly ExcalidrawElement[];
|
||||||
|
@ -27,19 +23,25 @@ export type DragInputCallbackType = ({
|
||||||
shouldKeepAspectRatio: boolean;
|
shouldKeepAspectRatio: boolean;
|
||||||
shouldChangeByStepSize: boolean;
|
shouldChangeByStepSize: boolean;
|
||||||
nextValue?: number;
|
nextValue?: number;
|
||||||
|
property: T;
|
||||||
|
scene: Scene;
|
||||||
|
originalAppState: AppState;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
interface StatsDragInputProps {
|
interface StatsDragInputProps<T extends StatsInputProperty> {
|
||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
value: number | "Mixed";
|
value: number | "Mixed";
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
shouldKeepAspectRatio?: boolean;
|
shouldKeepAspectRatio?: boolean;
|
||||||
dragInputCallback: DragInputCallbackType;
|
dragInputCallback: DragInputCallbackType<T>;
|
||||||
|
property: T;
|
||||||
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatsDragInput = ({
|
const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
dragInputCallback,
|
dragInputCallback,
|
||||||
|
@ -47,19 +49,48 @@ const StatsDragInput = ({
|
||||||
elements,
|
elements,
|
||||||
editable = true,
|
editable = true,
|
||||||
shouldKeepAspectRatio,
|
shouldKeepAspectRatio,
|
||||||
}: StatsDragInputProps) => {
|
property,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
}: StatsDragInputProps<T>) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const labelRef = useRef<HTMLDivElement>(null);
|
const labelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState(value.toString());
|
const [inputValue, setInputValue] = useState(value.toString());
|
||||||
|
|
||||||
useEffect(() => {
|
const stateRef = useRef<{
|
||||||
setInputValue(value.toString());
|
originalAppState: AppState;
|
||||||
}, [value, elements]);
|
originalElements: readonly ExcalidrawElement[];
|
||||||
|
lastUpdatedValue: string;
|
||||||
|
updatePending: boolean;
|
||||||
|
}>(null!);
|
||||||
|
if (!stateRef.current) {
|
||||||
|
stateRef.current = {
|
||||||
|
originalAppState: cloneJSON(appState),
|
||||||
|
originalElements: elements,
|
||||||
|
lastUpdatedValue: inputValue,
|
||||||
|
updatePending: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const handleInputValue = (v: string) => {
|
useEffect(() => {
|
||||||
const parsed = Number(v);
|
const inputValue = value.toString();
|
||||||
|
setInputValue(inputValue);
|
||||||
|
stateRef.current.lastUpdatedValue = inputValue;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleInputValue = (
|
||||||
|
updatedValue: string,
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
if (!stateRef.current.updatePending) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
stateRef.current.updatePending = false;
|
||||||
|
|
||||||
|
const parsed = Number(updatedValue);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
setInputValue(value.toString());
|
setInputValue(value.toString());
|
||||||
return;
|
return;
|
||||||
|
@ -74,6 +105,7 @@ const StatsDragInput = ({
|
||||||
// than the smallest delta allowed, which is 0.01
|
// than the smallest delta allowed, which is 0.01
|
||||||
// reason: idempotent to avoid unnecessary
|
// reason: idempotent to avoid unnecessary
|
||||||
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
||||||
|
stateRef.current.lastUpdatedValue = updatedValue;
|
||||||
dragInputCallback({
|
dragInputCallback({
|
||||||
accumulatedChange: 0,
|
accumulatedChange: 0,
|
||||||
instantChange: 0,
|
instantChange: 0,
|
||||||
|
@ -82,6 +114,9 @@ const StatsDragInput = ({
|
||||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||||
shouldChangeByStepSize: false,
|
shouldChangeByStepSize: false,
|
||||||
nextValue: rounded,
|
nextValue: rounded,
|
||||||
|
property,
|
||||||
|
scene,
|
||||||
|
originalAppState: appState,
|
||||||
});
|
});
|
||||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
||||||
}
|
}
|
||||||
|
@ -97,12 +132,28 @@ const StatsDragInput = ({
|
||||||
return () => {
|
return () => {
|
||||||
const nextValue = input?.value;
|
const nextValue = input?.value;
|
||||||
if (nextValue) {
|
if (nextValue) {
|
||||||
handleInputValueRef.current(nextValue);
|
handleInputValueRef.current(
|
||||||
|
nextValue,
|
||||||
|
stateRef.current.originalElements,
|
||||||
|
stateRef.current.originalAppState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [
|
||||||
|
// we need to track change of `editable` state as mount/unmount
|
||||||
|
// because react doesn't trigger `blur` when a an input is blurred due
|
||||||
|
// to being disabled (https://github.com/facebook/react/issues/9142).
|
||||||
|
// As such, if we keep rendering disabled inputs, then change in selection
|
||||||
|
// to an element that has a given property as non-editable would not trigger
|
||||||
|
// blur/unmount and wouldn't update the value.
|
||||||
|
editable,
|
||||||
|
]);
|
||||||
|
|
||||||
return editable ? (
|
if (!editable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("drag-input-container", !editable && "disabled")}
|
className={clsx("drag-input-container", !editable && "disabled")}
|
||||||
data-testid={label}
|
data-testid={label}
|
||||||
|
@ -125,6 +176,7 @@ const StatsDragInput = ({
|
||||||
let originalElements: ExcalidrawElement[] | null = null;
|
let originalElements: ExcalidrawElement[] | null = null;
|
||||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
||||||
null;
|
null;
|
||||||
|
const originalAppState: AppState = cloneJSON(appState);
|
||||||
|
|
||||||
let accumulatedChange: number | null = null;
|
let accumulatedChange: number | null = null;
|
||||||
|
|
||||||
|
@ -165,6 +217,9 @@ const StatsDragInput = ({
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||||
shouldChangeByStepSize: event.shiftKey,
|
shouldChangeByStepSize: event.shiftKey,
|
||||||
|
property,
|
||||||
|
scene,
|
||||||
|
originalAppState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +271,7 @@ const StatsDragInput = ({
|
||||||
eventTarget instanceof HTMLInputElement &&
|
eventTarget instanceof HTMLInputElement &&
|
||||||
event.key === KEYS.ENTER
|
event.key === KEYS.ENTER
|
||||||
) {
|
) {
|
||||||
handleInputValue(eventTarget.value);
|
handleInputValue(eventTarget.value, elements, appState);
|
||||||
app.focusContainer();
|
app.focusContainer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,23 +279,28 @@ const StatsDragInput = ({
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
stateRef.current.updatePending = true;
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
}}
|
}}
|
||||||
onFocus={(event) => {
|
onFocus={(event) => {
|
||||||
event.target.select();
|
event.target.select();
|
||||||
|
stateRef.current.originalElements = elements;
|
||||||
|
stateRef.current.originalAppState = cloneJSON(appState);
|
||||||
}}
|
}}
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
if (!inputValue) {
|
if (!inputValue) {
|
||||||
setInputValue(value.toString());
|
setInputValue(value.toString());
|
||||||
} else if (editable) {
|
} else if (editable) {
|
||||||
handleInputValue(event.target.value);
|
handleInputValue(
|
||||||
|
event.target.value,
|
||||||
|
stateRef.current.originalElements,
|
||||||
|
stateRef.current.originalAppState,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,50 @@
|
||||||
import type { ElementsMap, ExcalidrawTextElement } from "../../element/types";
|
import type { ExcalidrawTextElement } from "../../element/types";
|
||||||
import { refreshTextDimensions } from "../../element/newElement";
|
import { refreshTextDimensions } from "../../element/newElement";
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
import { fontSizeIcon } from "../icons";
|
import { fontSizeIcon } from "../icons";
|
||||||
|
import type Scene from "../../scene/Scene";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
import { isTextElement } from "../../element";
|
||||||
|
|
||||||
interface FontSizeProps {
|
interface FontSizeProps {
|
||||||
element: ExcalidrawTextElement;
|
element: ExcalidrawTextElement;
|
||||||
elementsMap: ElementsMap;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
|
property: "fontSize";
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_FONT_SIZE = 4;
|
const MIN_FONT_SIZE = 4;
|
||||||
const STEP_SIZE = 4;
|
const STEP_SIZE = 4;
|
||||||
|
|
||||||
const FontSize = ({ element, elementsMap }: FontSizeProps) => {
|
const handleFontSizeChange: DragInputCallbackType<
|
||||||
const handleFontSizeChange: DragInputCallbackType = ({
|
FontSizeProps["property"]
|
||||||
|
> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
scene,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement) {
|
if (origElement) {
|
||||||
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
if (!latestElement || !isTextElement(latestElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||||
|
|
||||||
const newElement = {
|
const newElement = {
|
||||||
...element,
|
...latestElement,
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
};
|
};
|
||||||
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
||||||
mutateElement(element, {
|
mutateElement(latestElement, {
|
||||||
...updates,
|
...updates,
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
});
|
});
|
||||||
|
@ -49,18 +62,19 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => {
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||||
}
|
}
|
||||||
const newElement = {
|
const newElement = {
|
||||||
...element,
|
...latestElement,
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
};
|
};
|
||||||
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
||||||
mutateElement(element, {
|
mutateElement(latestElement, {
|
||||||
...updates,
|
...updates,
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FontSize = ({ element, scene, appState, property }: FontSizeProps) => {
|
||||||
return (
|
return (
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
label="F"
|
label="F"
|
||||||
|
@ -68,6 +82,9 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => {
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handleFontSizeChange}
|
dragInputCallback={handleFontSizeChange}
|
||||||
icon={fontSizeIcon}
|
icon={fontSizeIcon}
|
||||||
|
appState={appState}
|
||||||
|
scene={scene}
|
||||||
|
property={property}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
import { isArrowElement } from "../../element/typeChecks";
|
import { isArrowElement } from "../../element/typeChecks";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { isInGroup } from "../../groups";
|
import { isInGroup } from "../../groups";
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
import { degreeToRadian, radianToDegree } from "../../math";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
|
@ -9,33 +9,42 @@ import { angleIcon } from "../icons";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiAngleProps {
|
interface MultiAngleProps {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
elementsMap: ElementsMap;
|
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
|
property: "angle";
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 15;
|
const STEP_SIZE = 15;
|
||||||
|
|
||||||
const MultiAngle = ({ elements, elementsMap, scene }: MultiAngleProps) => {
|
const handleDegreeChange: DragInputCallbackType<
|
||||||
const handleDegreeChange: DragInputCallbackType = ({
|
MultiAngleProps["property"]
|
||||||
|
> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
property,
|
||||||
const editableLatestIndividualElements = elements.filter(
|
scene,
|
||||||
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
}) => {
|
||||||
);
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const editableLatestIndividualElements = originalElements
|
||||||
|
.map((el) => elementsMap.get(el.id))
|
||||||
|
.filter((el) => el && !isInGroup(el) && isPropertyEditable(el, property));
|
||||||
const editableOriginalIndividualElements = originalElements.filter(
|
const editableOriginalIndividualElements = originalElements.filter(
|
||||||
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
(el) => !isInGroup(el) && isPropertyEditable(el, property),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextAngle = degreeToRadian(nextValue);
|
const nextAngle = degreeToRadian(nextValue);
|
||||||
|
|
||||||
for (const element of editableLatestIndividualElements) {
|
for (const element of editableLatestIndividualElements) {
|
||||||
|
if (!element) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
mutateElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
|
@ -57,6 +66,9 @@ const MultiAngle = ({ elements, elementsMap, scene }: MultiAngleProps) => {
|
||||||
|
|
||||||
for (let i = 0; i < editableLatestIndividualElements.length; i++) {
|
for (let i = 0; i < editableLatestIndividualElements.length; i++) {
|
||||||
const latestElement = editableLatestIndividualElements[i];
|
const latestElement = editableLatestIndividualElements[i];
|
||||||
|
if (!latestElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const originalElement = editableOriginalIndividualElements[i];
|
const originalElement = editableOriginalIndividualElements[i];
|
||||||
const originalAngleInDegrees =
|
const originalAngleInDegrees =
|
||||||
Math.round(radianToDegree(originalElement.angle) * 100) / 100;
|
Math.round(radianToDegree(originalElement.angle) * 100) / 100;
|
||||||
|
@ -85,8 +97,14 @@ const MultiAngle = ({ elements, elementsMap, scene }: MultiAngleProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MultiAngle = ({
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
property,
|
||||||
|
}: MultiAngleProps) => {
|
||||||
const editableLatestIndividualElements = elements.filter(
|
const editableLatestIndividualElements = elements.filter(
|
||||||
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
(el) => !isInGroup(el) && isPropertyEditable(el, "angle"),
|
||||||
);
|
);
|
||||||
|
@ -107,6 +125,9 @@ const MultiAngle = ({ elements, elementsMap, scene }: MultiAngleProps) => {
|
||||||
elements={elements}
|
elements={elements}
|
||||||
dragInputCallback={handleDegreeChange}
|
dragInputCallback={handleDegreeChange}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
appState={appState}
|
||||||
|
scene={scene}
|
||||||
|
property={property}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,10 +9,10 @@ import {
|
||||||
} from "../../element/textElement";
|
} from "../../element/textElement";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { Point } from "../../types";
|
import type { AppState, Point } from "../../types";
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
import { getElementsInAtomicUnit, resizeElement } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
|
||||||
|
@ -23,6 +23,7 @@ interface MultiDimensionProps {
|
||||||
elementsMap: ElementsMap;
|
elementsMap: ElementsMap;
|
||||||
atomicUnits: AtomicUnit[];
|
atomicUnits: AtomicUnit[];
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
const STEP_SIZE = 10;
|
||||||
|
@ -131,48 +132,20 @@ const resizeGroup = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MultiDimension = ({
|
const handleDimensionChange: DragInputCallbackType<
|
||||||
property,
|
MultiDimensionProps["property"]
|
||||||
elements,
|
> = ({
|
||||||
elementsMap,
|
|
||||||
atomicUnits,
|
|
||||||
scene,
|
|
||||||
}: MultiDimensionProps) => {
|
|
||||||
const sizes = useMemo(
|
|
||||||
() =>
|
|
||||||
atomicUnits.map((atomicUnit) => {
|
|
||||||
const elementsInUnit = getElementsInAtomicUnit(atomicUnit, elementsMap);
|
|
||||||
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(
|
|
||||||
elementsInUnit.map((el) => el.latest),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
Math.round((property === "width" ? x2 - x1 : y2 - y1) * 100) / 100
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const [el] = elementsInUnit;
|
|
||||||
|
|
||||||
return (
|
|
||||||
Math.round(
|
|
||||||
(property === "width" ? el.latest.width : el.latest.height) * 100,
|
|
||||||
) / 100
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[elementsMap, atomicUnits, property],
|
|
||||||
);
|
|
||||||
|
|
||||||
const value =
|
|
||||||
new Set(sizes).size === 1 ? Math.round(sizes[0] * 100) / 100 : "Mixed";
|
|
||||||
|
|
||||||
const editable = sizes.length > 0;
|
|
||||||
|
|
||||||
const handleDimensionChange: DragInputCallbackType = ({
|
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
|
originalElements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
originalAppState,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
scene,
|
||||||
|
property,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of atomicUnits) {
|
for (const atomicUnit of atomicUnits) {
|
||||||
const elementsInUnit = getElementsInAtomicUnit(
|
const elementsInUnit = getElementsInAtomicUnit(
|
||||||
|
@ -220,9 +193,7 @@ const MultiDimension = ({
|
||||||
isPropertyEditable(latestElement, property)
|
isPropertyEditable(latestElement, property)
|
||||||
) {
|
) {
|
||||||
let nextWidth =
|
let nextWidth =
|
||||||
property === "width"
|
property === "width" ? Math.max(0, nextValue) : latestElement.width;
|
||||||
? Math.max(0, nextValue)
|
|
||||||
: latestElement.width;
|
|
||||||
if (property === "width") {
|
if (property === "width") {
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
||||||
|
@ -250,10 +221,8 @@ const MultiDimension = ({
|
||||||
nextWidth,
|
nextWidth,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
false,
|
false,
|
||||||
latestElement,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -347,21 +316,50 @@ const MultiDimension = ({
|
||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||||
|
|
||||||
resizeElement(
|
resizeElement(nextWidth, nextHeight, false, origElement, elementsMap);
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
false,
|
|
||||||
latestElement,
|
|
||||||
origElement,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MultiDimension = ({
|
||||||
|
property,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
atomicUnits,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
}: MultiDimensionProps) => {
|
||||||
|
const sizes = useMemo(
|
||||||
|
() =>
|
||||||
|
atomicUnits.map((atomicUnit) => {
|
||||||
|
const elementsInUnit = getElementsInAtomicUnit(atomicUnit, elementsMap);
|
||||||
|
|
||||||
|
if (elementsInUnit.length > 1) {
|
||||||
|
const [x1, y1, x2, y2] = getCommonBounds(
|
||||||
|
elementsInUnit.map((el) => el.latest),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
Math.round((property === "width" ? x2 - x1 : y2 - y1) * 100) / 100
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const [el] = elementsInUnit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.round(
|
||||||
|
(property === "width" ? el.latest.width : el.latest.height) * 100,
|
||||||
|
) / 100
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[elementsMap, atomicUnits, property],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value =
|
||||||
|
new Set(sizes).size === 1 ? Math.round(sizes[0] * 100) / 100 : "Mixed";
|
||||||
|
|
||||||
|
const editable = sizes.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragInput
|
<DragInput
|
||||||
|
@ -370,6 +368,9 @@ const MultiDimension = ({
|
||||||
dragInputCallback={handleDimensionChange}
|
dragInputCallback={handleDimensionChange}
|
||||||
value={value}
|
value={value}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
appState={appState}
|
||||||
|
property={property}
|
||||||
|
scene={scene}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { isTextElement, refreshTextDimensions } from "../../element";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { isBoundToContainer } from "../../element/typeChecks";
|
import { isBoundToContainer } from "../../element/typeChecks";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
|
@ -12,40 +11,49 @@ import { fontSizeIcon } from "../icons";
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiFontSizeProps {
|
interface MultiFontSizeProps {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
elementsMap: ElementsMap;
|
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
|
property: "fontSize";
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_FONT_SIZE = 4;
|
const MIN_FONT_SIZE = 4;
|
||||||
const STEP_SIZE = 4;
|
const STEP_SIZE = 4;
|
||||||
|
|
||||||
const MultiFontSize = ({
|
const getApplicableTextElements = (
|
||||||
elements,
|
elements: readonly (ExcalidrawElement | undefined)[],
|
||||||
elementsMap,
|
) =>
|
||||||
scene,
|
elements.filter(
|
||||||
}: MultiFontSizeProps) => {
|
(el) =>
|
||||||
const latestTextElements = elements.filter(
|
el && !isInGroup(el) && isTextElement(el) && !isBoundToContainer(el),
|
||||||
(el) => !isInGroup(el) && isTextElement(el) && !isBoundToContainer(el),
|
|
||||||
) as ExcalidrawTextElement[];
|
) as ExcalidrawTextElement[];
|
||||||
const fontSizes = latestTextElements.map(
|
|
||||||
(textEl) => Math.round(textEl.fontSize * 10) / 10,
|
|
||||||
);
|
|
||||||
const value = new Set(fontSizes).size === 1 ? fontSizes[0] : "Mixed";
|
|
||||||
const editable = fontSizes.length > 0;
|
|
||||||
|
|
||||||
const handleFontSizeChange: DragInputCallbackType = ({
|
const handleFontSizeChange: DragInputCallbackType<
|
||||||
|
MultiFontSizeProps["property"]
|
||||||
|
> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
scene,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const latestTextElements = getApplicableTextElements(
|
||||||
|
originalElements.map((el) => elementsMap.get(el.id)),
|
||||||
|
);
|
||||||
|
|
||||||
if (nextValue) {
|
if (nextValue) {
|
||||||
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||||
|
|
||||||
for (const textElement of latestTextElements) {
|
for (const textElement of latestTextElements.map((el) =>
|
||||||
|
elementsMap.get(el.id),
|
||||||
|
)) {
|
||||||
|
if (!textElement || !isTextElement(textElement)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const newElement = {
|
const newElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
|
@ -98,7 +106,20 @@ const MultiFontSize = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MultiFontSize = ({
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
property,
|
||||||
|
}: MultiFontSizeProps) => {
|
||||||
|
const latestTextElements = getApplicableTextElements(elements);
|
||||||
|
const fontSizes = latestTextElements.map(
|
||||||
|
(textEl) => Math.round(textEl.fontSize * 10) / 10,
|
||||||
|
);
|
||||||
|
const value = new Set(fontSizes).size === 1 ? fontSizes[0] : "Mixed";
|
||||||
|
const editable = fontSizes.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
|
@ -108,6 +129,9 @@ const MultiFontSize = ({
|
||||||
dragInputCallback={handleFontSizeChange}
|
dragInputCallback={handleFontSizeChange}
|
||||||
value={value}
|
value={value}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
scene={scene}
|
||||||
|
property={property}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { rotate } from "../../math";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { getCommonBounds, isTextElement } from "../../element";
|
import { getCommonBounds, isTextElement } from "../../element";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiPositionProps {
|
interface MultiPositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
|
@ -15,6 +16,7 @@ interface MultiPositionProps {
|
||||||
elementsMap: ElementsMap;
|
elementsMap: ElementsMap;
|
||||||
atomicUnits: AtomicUnit[];
|
atomicUnits: AtomicUnit[];
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
const STEP_SIZE = 10;
|
||||||
|
@ -30,7 +32,6 @@ const moveElements = (
|
||||||
) => {
|
) => {
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
const origElement = originalElements[i];
|
const origElement = originalElements[i];
|
||||||
const latestElement = elements[i];
|
|
||||||
|
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
|
@ -53,7 +54,6 @@ const moveElements = (
|
||||||
moveElement(
|
moveElement(
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
latestElement,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
@ -65,7 +65,6 @@ const moveElements = (
|
||||||
const moveGroupTo = (
|
const moveGroupTo = (
|
||||||
nextX: number,
|
nextX: number,
|
||||||
nextY: number,
|
nextY: number,
|
||||||
latestElements: ExcalidrawElement[],
|
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
@ -74,9 +73,13 @@ const moveGroupTo = (
|
||||||
const offsetX = nextX - x1;
|
const offsetX = nextX - x1;
|
||||||
const offsetY = nextY - y1;
|
const offsetY = nextY - y1;
|
||||||
|
|
||||||
for (let i = 0; i < latestElements.length; i++) {
|
for (let i = 0; i < originalElements.length; i++) {
|
||||||
const origElement = originalElements[i];
|
const origElement = originalElements[i];
|
||||||
const latestElement = latestElements[i];
|
|
||||||
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
if (!latestElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// bound texts are moved with their containers
|
// bound texts are moved with their containers
|
||||||
if (!isTextElement(latestElement) || !latestElement.containerId) {
|
if (!isTextElement(latestElement) || !latestElement.containerId) {
|
||||||
|
@ -96,7 +99,6 @@ const moveGroupTo = (
|
||||||
moveElement(
|
moveElement(
|
||||||
topLeftX + offsetX,
|
topLeftX + offsetX,
|
||||||
topLeftY + offsetY,
|
topLeftY + offsetY,
|
||||||
latestElement,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
@ -106,46 +108,25 @@ const moveGroupTo = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MultiPosition = ({
|
const handlePositionChange: DragInputCallbackType<
|
||||||
property,
|
MultiPositionProps["property"]
|
||||||
elements,
|
> = ({
|
||||||
elementsMap,
|
|
||||||
atomicUnits,
|
|
||||||
scene,
|
|
||||||
}: MultiPositionProps) => {
|
|
||||||
const positions = useMemo(
|
|
||||||
() =>
|
|
||||||
atomicUnits.map((atomicUnit) => {
|
|
||||||
const elementsInUnit = Object.keys(atomicUnit)
|
|
||||||
.map((id) => elementsMap.get(id))
|
|
||||||
.filter((el) => el !== undefined) as ExcalidrawElement[];
|
|
||||||
|
|
||||||
// we're dealing with a group
|
|
||||||
if (elementsInUnit.length > 1) {
|
|
||||||
const [x1, y1] = getCommonBounds(elementsInUnit);
|
|
||||||
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
|
|
||||||
}
|
|
||||||
const [el] = elementsInUnit;
|
|
||||||
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
|
||||||
|
|
||||||
const [topLeftX, topLeftY] = rotate(el.x, el.y, cx, cy, el.angle);
|
|
||||||
|
|
||||||
return Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
|
||||||
}),
|
|
||||||
[atomicUnits, elementsMap, property],
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = new Set(positions).size === 1 ? positions[0] : "Mixed";
|
|
||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType = ({
|
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
property,
|
||||||
|
scene,
|
||||||
|
originalAppState,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of atomicUnits) {
|
for (const atomicUnit of getAtomicUnits(
|
||||||
|
originalElements,
|
||||||
|
originalAppState,
|
||||||
|
)) {
|
||||||
const elementsInUnit = getElementsInAtomicUnit(
|
const elementsInUnit = getElementsInAtomicUnit(
|
||||||
atomicUnit,
|
atomicUnit,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -162,7 +143,6 @@ const MultiPosition = ({
|
||||||
moveGroupTo(
|
moveGroupTo(
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
elementsInUnit.map((el) => el.latest),
|
|
||||||
elementsInUnit.map((el) => el.original),
|
elementsInUnit.map((el) => el.original),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
@ -192,7 +172,6 @@ const MultiPosition = ({
|
||||||
moveElement(
|
moveElement(
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
latestElement,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
@ -217,14 +196,46 @@ const MultiPosition = ({
|
||||||
property,
|
property,
|
||||||
changeInTopX,
|
changeInTopX,
|
||||||
changeInTopY,
|
changeInTopY,
|
||||||
elements,
|
originalElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MultiPosition = ({
|
||||||
|
property,
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
atomicUnits,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
}: MultiPositionProps) => {
|
||||||
|
const positions = useMemo(
|
||||||
|
() =>
|
||||||
|
atomicUnits.map((atomicUnit) => {
|
||||||
|
const elementsInUnit = Object.keys(atomicUnit)
|
||||||
|
.map((id) => elementsMap.get(id))
|
||||||
|
.filter((el) => el !== undefined) as ExcalidrawElement[];
|
||||||
|
|
||||||
|
// we're dealing with a group
|
||||||
|
if (elementsInUnit.length > 1) {
|
||||||
|
const [x1, y1] = getCommonBounds(elementsInUnit);
|
||||||
|
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
|
||||||
|
}
|
||||||
|
const [el] = elementsInUnit;
|
||||||
|
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
|
||||||
|
|
||||||
|
const [topLeftX, topLeftY] = rotate(el.x, el.y, cx, cy, el.angle);
|
||||||
|
|
||||||
|
return Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
||||||
|
}),
|
||||||
|
[atomicUnits, elementsMap, property],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = new Set(positions).size === 1 ? positions[0] : "Mixed";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
|
@ -232,6 +243,9 @@ const MultiPosition = ({
|
||||||
elements={elements}
|
elements={elements}
|
||||||
dragInputCallback={handlePositionChange}
|
dragInputCallback={handlePositionChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
property={property}
|
||||||
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,33 +3,29 @@ import { rotate } from "../../math";
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement } from "./utils";
|
||||||
|
import type Scene from "../../scene/Scene";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
property: "x" | "y";
|
property: "x" | "y";
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
elementsMap: ElementsMap;
|
elementsMap: ElementsMap;
|
||||||
|
scene: Scene;
|
||||||
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEP_SIZE = 10;
|
const STEP_SIZE = 10;
|
||||||
|
|
||||||
const Position = ({ property, element, elementsMap }: PositionProps) => {
|
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
const [topLeftX, topLeftY] = rotate(
|
|
||||||
element.x,
|
|
||||||
element.y,
|
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
element.angle,
|
|
||||||
);
|
|
||||||
const value =
|
|
||||||
Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
|
||||||
|
|
||||||
const handlePositionChange: DragInputCallbackType = ({
|
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
}) => {
|
property,
|
||||||
|
scene,
|
||||||
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
|
@ -49,7 +45,6 @@ const Position = ({ property, element, elementsMap }: PositionProps) => {
|
||||||
moveElement(
|
moveElement(
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
element,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
@ -81,12 +76,28 @@ const Position = ({ property, element, elementsMap }: PositionProps) => {
|
||||||
moveElement(
|
moveElement(
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
element,
|
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Position = ({
|
||||||
|
property,
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
scene,
|
||||||
|
appState,
|
||||||
|
}: PositionProps) => {
|
||||||
|
const [topLeftX, topLeftY] = rotate(
|
||||||
|
element.x,
|
||||||
|
element.y,
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const value =
|
||||||
|
Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
|
@ -94,6 +105,9 @@ const Position = ({ property, element, elementsMap }: PositionProps) => {
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handlePositionChange}
|
dragInputCallback={handlePositionChange}
|
||||||
value={value}
|
value={value}
|
||||||
|
property={property}
|
||||||
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,12 +11,7 @@ import Angle from "./Angle";
|
||||||
|
|
||||||
import FontSize from "./FontSize";
|
import FontSize from "./FontSize";
|
||||||
import MultiDimension from "./MultiDimension";
|
import MultiDimension from "./MultiDimension";
|
||||||
import {
|
import { elementsAreInSameGroup } from "../../groups";
|
||||||
elementsAreInSameGroup,
|
|
||||||
getElementsInGroup,
|
|
||||||
getSelectedGroupIds,
|
|
||||||
isInGroup,
|
|
||||||
} from "../../groups";
|
|
||||||
import MultiAngle from "./MultiAngle";
|
import MultiAngle from "./MultiAngle";
|
||||||
import MultiFontSize from "./MultiFontSize";
|
import MultiFontSize from "./MultiFontSize";
|
||||||
import Position from "./Position";
|
import Position from "./Position";
|
||||||
|
@ -24,8 +19,9 @@ import MultiPosition from "./MultiPosition";
|
||||||
import Collapsible from "./Collapsible";
|
import Collapsible from "./Collapsible";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||||
import type { AtomicUnit } from "./utils";
|
import { getAtomicUnits } from "./utils";
|
||||||
import { STATS_PANELS } from "../../constants";
|
import { STATS_PANELS } from "../../constants";
|
||||||
|
import { isTextElement } from "../../element";
|
||||||
|
|
||||||
interface StatsProps {
|
interface StatsProps {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
@ -106,21 +102,7 @@ export const StatsInner = memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const atomicUnits = useMemo(() => {
|
const atomicUnits = useMemo(() => {
|
||||||
const selectedGroupIds = getSelectedGroupIds(appState);
|
return getAtomicUnits(selectedElements, appState);
|
||||||
const _atomicUnits = selectedGroupIds.map((gid) => {
|
|
||||||
return getElementsInGroup(selectedElements, gid).reduce((acc, el) => {
|
|
||||||
acc[el.id] = true;
|
|
||||||
return acc;
|
|
||||||
}, {} as AtomicUnit);
|
|
||||||
});
|
|
||||||
selectedElements
|
|
||||||
.filter((el) => !isInGroup(el))
|
|
||||||
.forEach((el) => {
|
|
||||||
_atomicUnits.push({
|
|
||||||
[el.id]: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return _atomicUnits;
|
|
||||||
}, [selectedElements, appState]);
|
}, [selectedElements, appState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -206,30 +188,40 @@ export const StatsInner = memo(
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
property="x"
|
property="x"
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<Position
|
<Position
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
property="y"
|
property="y"
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<Dimension
|
<Dimension
|
||||||
property="width"
|
property="width"
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
elementsMap={elementsMap}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<Dimension
|
<Dimension
|
||||||
property="height"
|
property="height"
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
elementsMap={elementsMap}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<Angle
|
<Angle
|
||||||
|
property="angle"
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
elementsMap={elementsMap}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
{singleElement.type === "text" && (
|
{singleElement.type === "text" && (
|
||||||
<FontSize
|
<FontSize
|
||||||
|
property="fontSize"
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
elementsMap={elementsMap}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -254,6 +246,7 @@ export const StatsInner = memo(
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
atomicUnits={atomicUnits}
|
atomicUnits={atomicUnits}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<MultiPosition
|
<MultiPosition
|
||||||
property="y"
|
property="y"
|
||||||
|
@ -261,6 +254,7 @@ export const StatsInner = memo(
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
atomicUnits={atomicUnits}
|
atomicUnits={atomicUnits}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<MultiDimension
|
<MultiDimension
|
||||||
property="width"
|
property="width"
|
||||||
|
@ -268,6 +262,7 @@ export const StatsInner = memo(
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
atomicUnits={atomicUnits}
|
atomicUnits={atomicUnits}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<MultiDimension
|
<MultiDimension
|
||||||
property="height"
|
property="height"
|
||||||
|
@ -275,17 +270,22 @@ export const StatsInner = memo(
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
atomicUnits={atomicUnits}
|
atomicUnits={atomicUnits}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<MultiAngle
|
<MultiAngle
|
||||||
|
property="angle"
|
||||||
elements={multipleElements}
|
elements={multipleElements}
|
||||||
elementsMap={elementsMap}
|
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
|
{multipleElements.some((el) => isTextElement(el)) && (
|
||||||
<MultiFontSize
|
<MultiFontSize
|
||||||
|
property="fontSize"
|
||||||
elements={multipleElements}
|
elements={multipleElements}
|
||||||
elementsMap={elementsMap}
|
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -30,6 +30,12 @@ const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
|
||||||
let stats: HTMLElement | null = null;
|
let stats: HTMLElement | null = null;
|
||||||
let elementStats: HTMLElement | null | undefined = null;
|
let elementStats: HTMLElement | null | undefined = null;
|
||||||
|
|
||||||
|
const editInput = (input: HTMLInputElement, value: string) => {
|
||||||
|
input.focus();
|
||||||
|
fireEvent.change(input, { target: { value } });
|
||||||
|
input.blur();
|
||||||
|
};
|
||||||
|
|
||||||
const getStatsProperty = (label: string) => {
|
const getStatsProperty = (label: string) => {
|
||||||
if (elementStats) {
|
if (elementStats) {
|
||||||
const properties = elementStats?.querySelector(".statsItem");
|
const properties = elementStats?.querySelector(".statsItem");
|
||||||
|
@ -53,9 +59,7 @@ const testInputProperty = (
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
expect(input.value).toBe(initialValue.toString());
|
expect(input.value).toBe(initialValue.toString());
|
||||||
input?.focus();
|
editInput(input, String(nextValue));
|
||||||
input.value = nextValue.toString();
|
|
||||||
input?.blur();
|
|
||||||
if (property === "angle") {
|
if (property === "angle") {
|
||||||
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
||||||
} else if (property === "fontSize" && isTextElement(element)) {
|
} else if (property === "fontSize" && isTextElement(element)) {
|
||||||
|
@ -172,17 +176,13 @@ describe("stats for a generic element", () => {
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
expect(input.value).toBe(rectangle.width.toString());
|
expect(input.value).toBe(rectangle.width.toString());
|
||||||
input?.focus();
|
editInput(input, "123.123");
|
||||||
input.value = "123.123";
|
|
||||||
input?.blur();
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(rectangle.id).toBe(rectangleId);
|
expect(rectangle.id).toBe(rectangleId);
|
||||||
expect(input.value).toBe("123.12");
|
expect(input.value).toBe("123.12");
|
||||||
expect(rectangle.width).toBe(123.12);
|
expect(rectangle.width).toBe(123.12);
|
||||||
|
|
||||||
input?.focus();
|
editInput(input, "88.98766");
|
||||||
input.value = "88.98766";
|
|
||||||
input?.blur();
|
|
||||||
expect(input.value).toBe("88.99");
|
expect(input.value).toBe("88.99");
|
||||||
expect(rectangle.width).toBe(88.99);
|
expect(rectangle.width).toBe(88.99);
|
||||||
});
|
});
|
||||||
|
@ -335,9 +335,7 @@ describe("stats for a non-generic element", () => {
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
expect(input.value).toBe(text.fontSize.toString());
|
expect(input.value).toBe(text.fontSize.toString());
|
||||||
input?.focus();
|
editInput(input, "36");
|
||||||
input.value = "36";
|
|
||||||
input?.blur();
|
|
||||||
expect(text.fontSize).toBe(36);
|
expect(text.fontSize).toBe(36);
|
||||||
|
|
||||||
// cannot change width or height
|
// cannot change width or height
|
||||||
|
@ -347,9 +345,7 @@ describe("stats for a non-generic element", () => {
|
||||||
expect(height).toBeUndefined();
|
expect(height).toBeUndefined();
|
||||||
|
|
||||||
// min font size is 4
|
// min font size is 4
|
||||||
input.focus();
|
editInput(input, "0");
|
||||||
input.value = "0";
|
|
||||||
input.blur();
|
|
||||||
expect(text.fontSize).not.toBe(0);
|
expect(text.fontSize).not.toBe(0);
|
||||||
expect(text.fontSize).toBe(4);
|
expect(text.fontSize).toBe(4);
|
||||||
});
|
});
|
||||||
|
@ -471,16 +467,12 @@ describe("stats for multiple elements", () => {
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(angle.value).toBe("0");
|
expect(angle.value).toBe("0");
|
||||||
|
|
||||||
width.focus();
|
editInput(width, "250");
|
||||||
width.value = "250";
|
|
||||||
width.blur();
|
|
||||||
h.elements.forEach((el) => {
|
h.elements.forEach((el) => {
|
||||||
expect(el.width).toBe(250);
|
expect(el.width).toBe(250);
|
||||||
});
|
});
|
||||||
|
|
||||||
height.focus();
|
editInput(height, "450");
|
||||||
height.value = "450";
|
|
||||||
height.blur();
|
|
||||||
h.elements.forEach((el) => {
|
h.elements.forEach((el) => {
|
||||||
expect(el.height).toBe(450);
|
expect(el.height).toBe(450);
|
||||||
});
|
});
|
||||||
|
@ -501,7 +493,6 @@ describe("stats for multiple elements", () => {
|
||||||
mouse.up(200, 100);
|
mouse.up(200, 100);
|
||||||
|
|
||||||
const frame = API.createElement({
|
const frame = API.createElement({
|
||||||
id: "id0",
|
|
||||||
type: "frame",
|
type: "frame",
|
||||||
x: 150,
|
x: 150,
|
||||||
width: 150,
|
width: 150,
|
||||||
|
@ -545,17 +536,13 @@ describe("stats for multiple elements", () => {
|
||||||
expect(fontSize).not.toBeNull();
|
expect(fontSize).not.toBeNull();
|
||||||
|
|
||||||
// changing width does not affect text
|
// changing width does not affect text
|
||||||
width.focus();
|
editInput(width, "200");
|
||||||
width.value = "200";
|
|
||||||
width.blur();
|
|
||||||
|
|
||||||
expect(rectangle?.width).toBe(200);
|
expect(rectangle?.width).toBe(200);
|
||||||
expect(frame.width).toBe(200);
|
expect(frame.width).toBe(200);
|
||||||
expect(text?.width).not.toBe(200);
|
expect(text?.width).not.toBe(200);
|
||||||
|
|
||||||
angle.focus();
|
editInput(angle, "40");
|
||||||
angle.value = "40";
|
|
||||||
angle.blur();
|
|
||||||
|
|
||||||
const angleInRadian = degreeToRadian(40);
|
const angleInRadian = degreeToRadian(40);
|
||||||
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
||||||
|
@ -595,9 +582,7 @@ describe("stats for multiple elements", () => {
|
||||||
expect(x).not.toBeNull();
|
expect(x).not.toBeNull();
|
||||||
expect(Number(x.value)).toBe(x1);
|
expect(Number(x.value)).toBe(x1);
|
||||||
|
|
||||||
x.focus();
|
editInput(x, "300");
|
||||||
x.value = "300";
|
|
||||||
x.blur();
|
|
||||||
|
|
||||||
expect(h.elements[0].x).toBe(300);
|
expect(h.elements[0].x).toBe(300);
|
||||||
expect(h.elements[1].x).toBe(400);
|
expect(h.elements[1].x).toBe(400);
|
||||||
|
@ -610,9 +595,7 @@ describe("stats for multiple elements", () => {
|
||||||
expect(y).not.toBeNull();
|
expect(y).not.toBeNull();
|
||||||
expect(Number(y.value)).toBe(y1);
|
expect(Number(y.value)).toBe(y1);
|
||||||
|
|
||||||
y.focus();
|
editInput(y, "200");
|
||||||
y.value = "200";
|
|
||||||
y.blur();
|
|
||||||
|
|
||||||
expect(h.elements[0].y).toBe(200);
|
expect(h.elements[0].y).toBe(200);
|
||||||
expect(h.elements[1].y).toBe(300);
|
expect(h.elements[1].y).toBe(300);
|
||||||
|
@ -630,26 +613,20 @@ describe("stats for multiple elements", () => {
|
||||||
expect(height).not.toBeNull();
|
expect(height).not.toBeNull();
|
||||||
expect(Number(height.value)).toBe(200);
|
expect(Number(height.value)).toBe(200);
|
||||||
|
|
||||||
width.focus();
|
editInput(width, "400");
|
||||||
width.value = "400";
|
|
||||||
width.blur();
|
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||||
let newGroupWidth = x2 - x1;
|
let newGroupWidth = x2 - x1;
|
||||||
|
|
||||||
expect(newGroupWidth).toBeCloseTo(400, 4);
|
expect(newGroupWidth).toBeCloseTo(400, 4);
|
||||||
|
|
||||||
width.focus();
|
editInput(width, "300");
|
||||||
width.value = "300";
|
|
||||||
width.blur();
|
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||||
newGroupWidth = x2 - x1;
|
newGroupWidth = x2 - x1;
|
||||||
expect(newGroupWidth).toBeCloseTo(300, 4);
|
expect(newGroupWidth).toBeCloseTo(300, 4);
|
||||||
|
|
||||||
height.focus();
|
editInput(height, "500");
|
||||||
height.value = "500";
|
|
||||||
height.blur();
|
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||||
const newGroupHeight = y2 - y1;
|
const newGroupHeight = y2 - y1;
|
||||||
|
|
|
@ -17,9 +17,23 @@ import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
|
import {
|
||||||
|
getSelectedGroupIds,
|
||||||
|
getElementsInGroup,
|
||||||
|
isInGroup,
|
||||||
|
} from "../../groups";
|
||||||
import { rotate } from "../../math";
|
import { rotate } from "../../math";
|
||||||
|
import type { AppState } from "../../types";
|
||||||
import { getFontString } from "../../utils";
|
import { getFontString } from "../../utils";
|
||||||
|
|
||||||
|
export type StatsInputProperty =
|
||||||
|
| "x"
|
||||||
|
| "y"
|
||||||
|
| "width"
|
||||||
|
| "height"
|
||||||
|
| "angle"
|
||||||
|
| "fontSize";
|
||||||
|
|
||||||
export const SMALLEST_DELTA = 0.01;
|
export const SMALLEST_DELTA = 0.01;
|
||||||
|
|
||||||
export const isPropertyEditable = (
|
export const isPropertyEditable = (
|
||||||
|
@ -100,12 +114,14 @@ export const resizeElement = (
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
keepAspectRatio: boolean,
|
keepAspectRatio: boolean,
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
originalElementsMap: Map<string, ExcalidrawElement>,
|
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
if (!latestElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
let boundTextFont: { fontSize?: number } = {};
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
|
|
||||||
|
@ -181,12 +197,15 @@ export const resizeElement = (
|
||||||
export const moveElement = (
|
export const moveElement = (
|
||||||
newTopLeftX: number,
|
newTopLeftX: number,
|
||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
|
const latestElement = elementsMap.get(originalElement.id);
|
||||||
|
if (!latestElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
originalElement.x + originalElement.width / 2,
|
originalElement.x + originalElement.width / 2,
|
||||||
originalElement.y + originalElement.height / 2,
|
originalElement.y + originalElement.height / 2,
|
||||||
|
@ -236,3 +255,24 @@ export const moveElement = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAtomicUnits = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
const selectedGroupIds = getSelectedGroupIds(appState);
|
||||||
|
const _atomicUnits = selectedGroupIds.map((gid) => {
|
||||||
|
return getElementsInGroup(targetElements, gid).reduce((acc, el) => {
|
||||||
|
acc[el.id] = true;
|
||||||
|
return acc;
|
||||||
|
}, {} as AtomicUnit);
|
||||||
|
});
|
||||||
|
targetElements
|
||||||
|
.filter((el) => !isInGroup(el))
|
||||||
|
.forEach((el) => {
|
||||||
|
_atomicUnits.push({
|
||||||
|
[el.id]: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return _atomicUnits;
|
||||||
|
};
|
||||||
|
|
|
@ -90,7 +90,7 @@ const shouldResetImageFilter = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCanvasPadding = (element: ExcalidrawElement) =>
|
const getCanvasPadding = (element: ExcalidrawElement) =>
|
||||||
element.type === "freedraw" ? element.strokeWidth * 12 : 20;
|
element.type === "freedraw" ? element.strokeWidth * 12 : 200;
|
||||||
|
|
||||||
export const getRenderOpacity = (
|
export const getRenderOpacity = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import type { Drawable } from "roughjs/bin/core";
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
|
||||||
NonDeletedElementsMap,
|
NonDeletedElementsMap,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
|
@ -96,10 +95,6 @@ export type SceneScroll = {
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Scene {
|
|
||||||
elements: ExcalidrawTextElement[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExportType =
|
export type ExportType =
|
||||||
| "png"
|
| "png"
|
||||||
| "clipboard"
|
| "clipboard"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue