handle bound texts

editable-element-stats
Ryan Di 8 months ago
parent be65ac7f22
commit c68c2be44c

@ -1,5 +1,7 @@
import { mutateElement } from "../../element/mutateElement";
import type { ExcalidrawElement } from "../../element/types";
import { getBoundTextElement } from "../../element/textElement";
import { isArrowElement } from "../../element/typeChecks";
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
import { degreeToRadian, radianToDegree } from "../../math";
import DragInput from "./DragInput";
import type { DragInputCallbackType } from "./DragInput";
@ -7,19 +9,18 @@ import { getStepSizedValue, isPropertyEditable } from "./utils";
interface AngleProps {
element: ExcalidrawElement;
elementsMap: ElementsMap;
}
const STEP_SIZE = 15;
const Angle = ({ element }: AngleProps) => {
const handleDegreeChange: DragInputCallbackType = (
const Angle = ({ element, elementsMap }: AngleProps) => {
const handleDegreeChange: DragInputCallbackType = ({
accumulatedChange,
instantChange,
stateAtStart,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
) => {
}) => {
const _stateAtStart = stateAtStart[0];
if (_stateAtStart) {
if (nextValue !== undefined) {
@ -27,6 +28,12 @@ const Angle = ({ element }: AngleProps) => {
mutateElement(element, {
angle: nextAngle,
});
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && !isArrowElement(element)) {
mutateElement(boundTextElement, { angle: nextAngle });
}
return;
}
@ -38,13 +45,19 @@ const Angle = ({ element }: AngleProps) => {
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
}
nextAngleInDegrees =
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
const nextAngle = degreeToRadian(nextAngleInDegrees);
mutateElement(element, {
angle: degreeToRadian(
nextAngleInDegrees < 0
? nextAngleInDegrees + 360
: nextAngleInDegrees,
),
angle: nextAngle,
});
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && !isArrowElement(element)) {
mutateElement(boundTextElement, { angle: nextAngle });
}
}
};

@ -1,13 +1,26 @@
import type { ExcalidrawElement } from "../../element/types";
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
import DragInput from "./DragInput";
import type { DragInputCallbackType } from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils";
import { mutateElement } from "../../element/mutateElement";
import { rescalePointsInElement } from "../../element/resizeElements";
import {
measureFontSizeFromWidth,
rescalePointsInElement,
} from "../../element/resizeElements";
import {
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElement,
getBoundTextMaxWidth,
handleBindTextResize,
} from "../../element/textElement";
import { getFontString } from "../../utils";
import { updateBoundElements } from "../../element/binding";
interface DimensionDragInputProps {
property: "width" | "height";
element: ExcalidrawElement;
elementsMap: ElementsMap;
}
const STEP_SIZE = 10;
@ -51,13 +64,16 @@ export const newOrigin = (
};
};
const getResizedUpdates = (
const resizeElement = (
nextWidth: number,
nextHeight: number,
keepAspectRatio: boolean,
latestState: ExcalidrawElement,
stateAtStart: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: Map<string, ExcalidrawElement>,
) => {
return {
mutateElement(latestState, {
...newOrigin(
latestState.x,
latestState.y,
@ -70,18 +86,72 @@ const getResizedUpdates = (
width: nextWidth,
height: nextHeight,
...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, true),
});
let boundTextFont: { fontSize?: number } = {};
const boundTextElement = getBoundTextElement(latestState, elementsMap);
if (boundTextElement) {
boundTextFont = {
fontSize: boundTextElement.fontSize,
};
if (keepAspectRatio) {
const updatedElement = {
...latestState,
width: nextWidth,
height: nextHeight,
};
const nextFont = measureFontSizeFromWidth(
boundTextElement,
elementsMap,
getBoundTextMaxWidth(updatedElement, boundTextElement),
);
boundTextFont = {
fontSize: nextFont?.size ?? boundTextElement.fontSize,
};
} else {
const minWidth = getApproxMinLineWidth(
getFontString(boundTextElement),
boundTextElement.lineHeight,
);
const minHeight = getApproxMinLineHeight(
boundTextElement.fontSize,
boundTextElement.lineHeight,
);
nextWidth = Math.max(nextWidth, minWidth);
nextHeight = Math.max(nextHeight, minHeight);
}
}
updateBoundElements(latestState, elementsMap, {
newSize: {
width: nextWidth,
height: nextHeight,
},
});
if (boundTextElement && boundTextFont) {
mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize,
});
}
handleBindTextResize(latestState, elementsMap, "e", keepAspectRatio);
};
const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
const handleDimensionChange: DragInputCallbackType = (
const DimensionDragInput = ({
property,
element,
elementsMap,
}: DimensionDragInputProps) => {
const handleDimensionChange: DragInputCallbackType = ({
accumulatedChange,
instantChange,
stateAtStart,
originalElementsMap,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
) => {
}) => {
const _stateAtStart = stateAtStart[0];
if (_stateAtStart) {
const keepAspectRatio =
@ -106,10 +176,16 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
0,
);
mutateElement(
resizeElement(
nextWidth,
nextHeight,
keepAspectRatio,
element,
getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart),
_stateAtStart,
elementsMap,
originalElementsMap,
);
return;
}
const changeInWidth = property === "width" ? accumulatedChange : 0;
@ -141,9 +217,14 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
}
}
mutateElement(
resizeElement(
nextWidth,
nextHeight,
keepAspectRatio,
element,
getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart),
_stateAtStart,
elementsMap,
originalElementsMap,
);
}
};

@ -2,21 +2,30 @@ import { useEffect, useMemo, useRef, useState } from "react";
import throttle from "lodash.throttle";
import { EVENT } from "../../constants";
import { KEYS } from "../../keys";
import type { ExcalidrawElement } from "../../element/types";
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
import { deepCopyElement } from "../../element/newElement";
import "./DragInput.scss";
import clsx from "clsx";
import { useApp } from "../App";
export type DragInputCallbackType = (
accumulatedChange: number,
instantChange: number,
stateAtStart: ExcalidrawElement[],
shouldKeepAspectRatio: boolean,
shouldChangeByStepSize: boolean,
nextValue?: number,
) => void;
export type DragInputCallbackType = ({
accumulatedChange,
instantChange,
stateAtStart,
originalElementsMap,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
}: {
accumulatedChange: number;
instantChange: number;
stateAtStart: ExcalidrawElement[];
originalElementsMap: ElementsMap;
shouldKeepAspectRatio: boolean;
shouldChangeByStepSize: boolean;
nextValue?: number;
}) => void;
interface StatsDragInputProps {
label: string | React.ReactNode;
@ -67,6 +76,8 @@ const StatsDragInput = ({
} | null = null;
let stateAtStart: ExcalidrawElement[] | null = null;
let originalElementsMap: Map<string, ExcalidrawElement> | null =
null;
let accumulatedChange: number | null = null;
@ -79,6 +90,15 @@ const StatsDragInput = ({
);
}
if (!originalElementsMap) {
originalElementsMap = app.scene
.getNonDeletedElements()
.reduce((acc, element) => {
acc.set(element.id, deepCopyElement(element));
return acc;
}, new Map() as ElementsMap);
}
if (!accumulatedChange) {
accumulatedChange = 0;
}
@ -87,13 +107,14 @@ const StatsDragInput = ({
const instantChange = event.clientX - lastPointer.x;
accumulatedChange += instantChange;
cbThrottled(
cbThrottled({
accumulatedChange,
instantChange,
stateAtStart,
shouldKeepAspectRatio!!,
event.shiftKey,
);
originalElementsMap,
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
shouldChangeByStepSize: event.shiftKey,
});
}
lastPointer = {
@ -117,6 +138,7 @@ const StatsDragInput = ({
lastPointer = null;
accumulatedChange = null;
stateAtStart = null;
originalElementsMap = null;
document.body.classList.remove("dragResize");
},
@ -149,14 +171,16 @@ const StatsDragInput = ({
setInputValue(value.toString());
return;
}
dragInputCallback(
0,
0,
elements,
shouldKeepAspectRatio!!,
false,
v,
);
dragInputCallback({
accumulatedChange: 0,
instantChange: 0,
stateAtStart: elements,
originalElementsMap: app.scene.getNonDeletedElementsMap(),
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
shouldChangeByStepSize: false,
nextValue: v,
});
app.store.shouldCaptureIncrement();
eventTarget.blur();
}

@ -14,14 +14,12 @@ const MIN_FONT_SIZE = 4;
const STEP_SIZE = 4;
const FontSize = ({ element, elementsMap }: FontSizeProps) => {
const handleFontSizeChange: DragInputCallbackType = (
const handleFontSizeChange: DragInputCallbackType = ({
accumulatedChange,
instantChange,
stateAtStart,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
) => {
}) => {
const _stateAtStart = stateAtStart[0];
if (_stateAtStart) {
if (nextValue) {

@ -1,7 +1,12 @@
import { getCommonBounds } from "../../element";
import { getCommonBounds, isTextElement } from "../../element";
import { updateBoundElements } from "../../element/binding";
import { mutateElement } from "../../element/mutateElement";
import { rescalePointsInElement } from "../../element/resizeElements";
import type { ExcalidrawElement } from "../../element/types";
import {
getBoundTextElement,
handleBindTextResize,
} from "../../element/textElement";
import type { ElementsMap, ExcalidrawElement } from "../../element/types";
import DragInput from "./DragInput";
import type { DragInputCallbackType } from "./DragInput";
import { getStepSizedValue } from "./utils";
@ -9,6 +14,7 @@ import { getStepSizedValue } from "./utils";
interface MultiDimensionProps {
property: "width" | "height";
elements: ExcalidrawElement[];
elementsMap: ElementsMap;
}
const STEP_SIZE = 10;
@ -32,18 +38,66 @@ const getResizedUpdates = (
x,
y,
...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, false),
...(isTextElement(stateAtStart)
? { fontSize: stateAtStart.fontSize * scale }
: {}),
};
};
const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
const handleDimensionChange: DragInputCallbackType = (
const resizeElement = (
anchorX: number,
anchorY: number,
property: MultiDimensionProps["property"],
scale: number,
latestElement: ExcalidrawElement,
origElement: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: ElementsMap,
shouldInformMutation: boolean,
) => {
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
mutateElement(latestElement, updates, shouldInformMutation);
const boundTextElement = getBoundTextElement(
origElement,
originalElementsMap,
);
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale;
updateBoundElements(latestElement, elementsMap, {
newSize: { width: updates.width, height: updates.height },
});
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
mutateElement(
latestBoundTextElement,
{
fontSize: newFontSize,
},
shouldInformMutation,
);
handleBindTextResize(
latestElement,
elementsMap,
property === "width" ? "e" : "s",
true,
);
}
}
};
const MultiDimension = ({
property,
elements,
elementsMap,
}: MultiDimensionProps) => {
const handleDimensionChange: DragInputCallbackType = ({
accumulatedChange,
instantChange,
stateAtStart,
shouldKeepAspectRatio,
originalElementsMap,
shouldChangeByStepSize,
nextValue,
) => {
}) => {
const [x1, y1, x2, y2] = getCommonBounds(stateAtStart);
const initialWidth = x2 - x1;
const initialHeight = y2 - y1;
@ -60,15 +114,21 @@ const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
let i = 0;
while (i < stateAtStart.length) {
const element = elements[i];
const latestElement = elements[i];
const origElement = stateAtStart[i];
// it should never happen that element and origElement are different
// but check just in case
if (element.id === origElement.id) {
mutateElement(
element,
getResizedUpdates(anchorX, anchorY, scale, origElement),
if (latestElement.id === origElement.id) {
resizeElement(
anchorX,
anchorY,
property,
scale,
latestElement,
origElement,
elementsMap,
originalElementsMap,
i === stateAtStart.length - 1,
);
}
@ -113,13 +173,19 @@ const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
let i = 0;
while (i < stateAtStart.length) {
const element = elements[i];
const latestElement = elements[i];
const origElement = stateAtStart[i];
if (element.id === origElement.id) {
mutateElement(
element,
getResizedUpdates(anchorX, anchorY, scale, origElement),
if (latestElement.id === origElement.id) {
resizeElement(
anchorX,
anchorY,
property,
scale,
latestElement,
origElement,
elementsMap,
originalElementsMap,
i === stateAtStart.length - 1,
);
}

@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { getCommonBounds } from "../../element/bounds";
import type { NonDeletedExcalidrawElement } from "../../element/types";
import { t } from "../../i18n";
import { getTargetElements } from "../../scene";
import { getSelectedElements } from "../../scene";
import type Scene from "../../scene/Scene";
import type { AppState, ExcalidrawProps } from "../../types";
import { CloseIcon } from "../icons";
@ -29,7 +29,14 @@ export const Stats = (props: StatsProps) => {
const elements = props.scene.getNonDeletedElements();
const elementsMap = props.scene.getNonDeletedElementsMap();
const sceneNonce = props.scene.getSceneNonce();
const selectedElements = getTargetElements(elements, props.appState);
// const selectedElements = getTargetElements(elements, props.appState);
const selectedElements = getSelectedElements(
props.scene.getNonDeletedElementsMap(),
props.appState,
{
includeBoundTextElement: false,
},
);
const singleElement =
selectedElements.length === 1 ? selectedElements[0] : null;
@ -112,9 +119,17 @@ export const Stats = (props: StatsProps) => {
</div>
<div className="statsItem">
<Dimension property="width" element={singleElement} />
<Dimension property="height" element={singleElement} />
<Angle element={singleElement} />
<Dimension
property="width"
element={singleElement}
elementsMap={elementsMap}
/>
<Dimension
property="height"
element={singleElement}
elementsMap={elementsMap}
/>
<Angle element={singleElement} elementsMap={elementsMap} />
{singleElement.type === "text" && (
<FontSize
element={singleElement}
@ -142,10 +157,12 @@ export const Stats = (props: StatsProps) => {
<MultiDimension
property="width"
elements={multipleElements}
elementsMap={elementsMap}
/>
<MultiDimension
property="height"
elements={multipleElements}
elementsMap={elementsMap}
/>
</div>
</div>

@ -199,7 +199,7 @@ export const rescalePointsInElement = (
}
: {};
const measureFontSizeFromWidth = (
export const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
nextWidth: number,

Loading…
Cancel
Save