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 { 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 { degreeToRadian, radianToDegree } from "../../math";
import DragInput from "./DragInput"; import DragInput from "./DragInput";
import type { DragInputCallbackType } from "./DragInput"; import type { DragInputCallbackType } from "./DragInput";
@ -7,19 +9,18 @@ import { getStepSizedValue, isPropertyEditable } from "./utils";
interface AngleProps { interface AngleProps {
element: ExcalidrawElement; element: ExcalidrawElement;
elementsMap: ElementsMap;
} }
const STEP_SIZE = 15; const STEP_SIZE = 15;
const Angle = ({ element }: AngleProps) => { const Angle = ({ element, elementsMap }: AngleProps) => {
const handleDegreeChange: DragInputCallbackType = ( const handleDegreeChange: DragInputCallbackType = ({
accumulatedChange, accumulatedChange,
instantChange,
stateAtStart, stateAtStart,
shouldKeepAspectRatio,
shouldChangeByStepSize, shouldChangeByStepSize,
nextValue, nextValue,
) => { }) => {
const _stateAtStart = stateAtStart[0]; const _stateAtStart = stateAtStart[0];
if (_stateAtStart) { if (_stateAtStart) {
if (nextValue !== undefined) { if (nextValue !== undefined) {
@ -27,6 +28,12 @@ const Angle = ({ element }: AngleProps) => {
mutateElement(element, { mutateElement(element, {
angle: nextAngle, angle: nextAngle,
}); });
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && !isArrowElement(element)) {
mutateElement(boundTextElement, { angle: nextAngle });
}
return; return;
} }
@ -38,13 +45,19 @@ const Angle = ({ element }: AngleProps) => {
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE); nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
} }
nextAngleInDegrees =
nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
const nextAngle = degreeToRadian(nextAngleInDegrees);
mutateElement(element, { mutateElement(element, {
angle: degreeToRadian( angle: nextAngle,
nextAngleInDegrees < 0
? nextAngleInDegrees + 360
: nextAngleInDegrees,
),
}); });
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 DragInput from "./DragInput";
import type { DragInputCallbackType } from "./DragInput"; import type { DragInputCallbackType } from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils"; import { getStepSizedValue, isPropertyEditable } from "./utils";
import { mutateElement } from "../../element/mutateElement"; 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 { interface DimensionDragInputProps {
property: "width" | "height"; property: "width" | "height";
element: ExcalidrawElement; element: ExcalidrawElement;
elementsMap: ElementsMap;
} }
const STEP_SIZE = 10; const STEP_SIZE = 10;
@ -51,13 +64,16 @@ export const newOrigin = (
}; };
}; };
const getResizedUpdates = ( const resizeElement = (
nextWidth: number, nextWidth: number,
nextHeight: number, nextHeight: number,
keepAspectRatio: boolean,
latestState: ExcalidrawElement, latestState: ExcalidrawElement,
stateAtStart: ExcalidrawElement, stateAtStart: ExcalidrawElement,
elementsMap: ElementsMap,
originalElementsMap: Map<string, ExcalidrawElement>,
) => { ) => {
return { mutateElement(latestState, {
...newOrigin( ...newOrigin(
latestState.x, latestState.x,
latestState.y, latestState.y,
@ -70,18 +86,72 @@ const getResizedUpdates = (
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
...rescalePointsInElement(stateAtStart, nextWidth, nextHeight, true), ...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 DimensionDragInput = ({
const handleDimensionChange: DragInputCallbackType = ( property,
element,
elementsMap,
}: DimensionDragInputProps) => {
const handleDimensionChange: DragInputCallbackType = ({
accumulatedChange, accumulatedChange,
instantChange,
stateAtStart, stateAtStart,
originalElementsMap,
shouldKeepAspectRatio, shouldKeepAspectRatio,
shouldChangeByStepSize, shouldChangeByStepSize,
nextValue, nextValue,
) => { }) => {
const _stateAtStart = stateAtStart[0]; const _stateAtStart = stateAtStart[0];
if (_stateAtStart) { if (_stateAtStart) {
const keepAspectRatio = const keepAspectRatio =
@ -106,10 +176,16 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
0, 0,
); );
mutateElement( resizeElement(
nextWidth,
nextHeight,
keepAspectRatio,
element, element,
getResizedUpdates(nextWidth, nextHeight, element, _stateAtStart), _stateAtStart,
elementsMap,
originalElementsMap,
); );
return; return;
} }
const changeInWidth = property === "width" ? accumulatedChange : 0; const changeInWidth = property === "width" ? accumulatedChange : 0;
@ -141,9 +217,14 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
} }
} }
mutateElement( resizeElement(
nextWidth,
nextHeight,
keepAspectRatio,
element, 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 throttle from "lodash.throttle";
import { EVENT } from "../../constants"; import { EVENT } from "../../constants";
import { KEYS } from "../../keys"; import { KEYS } from "../../keys";
import type { 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 "./DragInput.scss";
import clsx from "clsx"; import clsx from "clsx";
import { useApp } from "../App"; import { useApp } from "../App";
export type DragInputCallbackType = ( export type DragInputCallbackType = ({
accumulatedChange: number, accumulatedChange,
instantChange: number, instantChange,
stateAtStart: ExcalidrawElement[], stateAtStart,
shouldKeepAspectRatio: boolean, originalElementsMap,
shouldChangeByStepSize: boolean, shouldKeepAspectRatio,
nextValue?: number, shouldChangeByStepSize,
) => void; nextValue,
}: {
accumulatedChange: number;
instantChange: number;
stateAtStart: ExcalidrawElement[];
originalElementsMap: ElementsMap;
shouldKeepAspectRatio: boolean;
shouldChangeByStepSize: boolean;
nextValue?: number;
}) => void;
interface StatsDragInputProps { interface StatsDragInputProps {
label: string | React.ReactNode; label: string | React.ReactNode;
@ -67,6 +76,8 @@ const StatsDragInput = ({
} | null = null; } | null = null;
let stateAtStart: ExcalidrawElement[] | null = null; let stateAtStart: ExcalidrawElement[] | null = null;
let originalElementsMap: Map<string, ExcalidrawElement> | null =
null;
let accumulatedChange: number | 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) { if (!accumulatedChange) {
accumulatedChange = 0; accumulatedChange = 0;
} }
@ -87,13 +107,14 @@ const StatsDragInput = ({
const instantChange = event.clientX - lastPointer.x; const instantChange = event.clientX - lastPointer.x;
accumulatedChange += instantChange; accumulatedChange += instantChange;
cbThrottled( cbThrottled({
accumulatedChange, accumulatedChange,
instantChange, instantChange,
stateAtStart, stateAtStart,
shouldKeepAspectRatio!!, originalElementsMap,
event.shiftKey, shouldKeepAspectRatio: shouldKeepAspectRatio!!,
); shouldChangeByStepSize: event.shiftKey,
});
} }
lastPointer = { lastPointer = {
@ -117,6 +138,7 @@ const StatsDragInput = ({
lastPointer = null; lastPointer = null;
accumulatedChange = null; accumulatedChange = null;
stateAtStart = null; stateAtStart = null;
originalElementsMap = null;
document.body.classList.remove("dragResize"); document.body.classList.remove("dragResize");
}, },
@ -149,14 +171,16 @@ const StatsDragInput = ({
setInputValue(value.toString()); setInputValue(value.toString());
return; return;
} }
dragInputCallback(
0, dragInputCallback({
0, accumulatedChange: 0,
elements, instantChange: 0,
shouldKeepAspectRatio!!, stateAtStart: elements,
false, originalElementsMap: app.scene.getNonDeletedElementsMap(),
v, shouldKeepAspectRatio: shouldKeepAspectRatio!!,
); shouldChangeByStepSize: false,
nextValue: v,
});
app.store.shouldCaptureIncrement(); app.store.shouldCaptureIncrement();
eventTarget.blur(); eventTarget.blur();
} }

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

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

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

Loading…
Cancel
Save