handle bound texts
This commit is contained in:
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 DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
|
||||
const handleDimensionChange: DragInputCallbackType = (
|
||||
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,
|
||||
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…
x
Reference in New Issue
Block a user