change dimension for multiple elements

editable-element-stats
Ryan Di 9 months ago
parent 7f4659339b
commit f0c1e9707a

@ -20,17 +20,18 @@ const Angle = ({ element }: AngleProps) => {
shouldChangeByStepSize,
nextValue,
) => {
if (nextValue !== undefined) {
const nextAngle = degreeToRadian(nextValue);
mutateElement(element, {
angle: nextAngle,
});
return;
}
const _stateAtStart = stateAtStart[0];
if (_stateAtStart) {
if (nextValue !== undefined) {
const nextAngle = degreeToRadian(nextValue);
mutateElement(element, {
angle: nextAngle,
});
return;
}
if (stateAtStart) {
const originalAngleInDegrees =
Math.round(radianToDegree(stateAtStart.angle) * 100) / 100;
Math.round(radianToDegree(_stateAtStart.angle) * 100) / 100;
const changeInDegrees = Math.round(accumulatedChange);
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
if (shouldChangeByStepSize) {
@ -51,7 +52,7 @@ const Angle = ({ element }: AngleProps) => {
<DragInput
label="A"
value={Math.round(radianToDegree(element.angle) * 100) / 100}
element={element}
elements={[element]}
dragInputCallback={handleDegreeChange}
editable={isPropertyEditable(element, "angle")}
/>

@ -14,7 +14,7 @@ const _shouldKeepAspectRatio = (element: ExcalidrawElement) => {
return element.type === "image";
};
const newOrigin = (
export const newOrigin = (
x1: number,
y1: number,
w1: number,
@ -59,10 +59,11 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
shouldChangeByStepSize,
nextValue,
) => {
if (stateAtStart) {
const _stateAtStart = stateAtStart[0];
if (_stateAtStart) {
const keepAspectRatio =
shouldKeepAspectRatio || _shouldKeepAspectRatio(element);
const aspectRatio = stateAtStart.width / stateAtStart.height;
const aspectRatio = _stateAtStart.width / _stateAtStart.height;
if (nextValue !== undefined) {
const nextWidth = Math.max(
@ -70,7 +71,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
? nextValue
: keepAspectRatio
? nextValue * aspectRatio
: stateAtStart.width,
: _stateAtStart.width,
0,
);
const nextHeight = Math.max(
@ -78,7 +79,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
? nextValue
: keepAspectRatio
? nextValue / aspectRatio
: stateAtStart.height,
: _stateAtStart.height,
0,
);
@ -100,7 +101,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
const changeInWidth = property === "width" ? accumulatedChange : 0;
const changeInHeight = property === "height" ? accumulatedChange : 0;
let nextWidth = Math.max(0, stateAtStart.width + changeInWidth);
let nextWidth = Math.max(0, _stateAtStart.width + changeInWidth);
if (property === "width") {
if (shouldChangeByStepSize) {
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
@ -109,7 +110,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
}
}
let nextHeight = Math.max(0, stateAtStart.height + changeInHeight);
let nextHeight = Math.max(0, _stateAtStart.height + changeInHeight);
if (property === "height") {
if (shouldChangeByStepSize) {
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
@ -130,13 +131,13 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
width: nextWidth,
height: nextHeight,
...newOrigin(
stateAtStart.x,
stateAtStart.y,
stateAtStart.width,
stateAtStart.height,
_stateAtStart.x,
_stateAtStart.y,
_stateAtStart.width,
_stateAtStart.height,
nextWidth,
nextHeight,
stateAtStart.angle,
_stateAtStart.angle,
),
});
}
@ -145,7 +146,7 @@ const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
return (
<DragInput
label={property === "width" ? "W" : "H"}
element={element}
elements={[element]}
dragInputCallback={handleDimensionChange}
value={
Math.round(

@ -11,7 +11,7 @@ import clsx from "clsx";
export type DragInputCallbackType = (
accumulatedChange: number,
instantChange: number,
stateAtStart: ExcalidrawElement,
stateAtStart: ExcalidrawElement[],
shouldKeepAspectRatio: boolean,
shouldChangeByStepSize: boolean,
nextValue?: number,
@ -20,7 +20,7 @@ export type DragInputCallbackType = (
interface StatsDragInputProps {
label: string | React.ReactNode;
value: number;
element: ExcalidrawElement;
elements: ExcalidrawElement[];
editable?: boolean;
shouldKeepAspectRatio?: boolean;
dragInputCallback: DragInputCallbackType;
@ -30,7 +30,7 @@ const StatsDragInput = ({
label,
dragInputCallback,
value,
element,
elements,
editable = true,
shouldKeepAspectRatio,
}: StatsDragInputProps) => {
@ -64,7 +64,7 @@ const StatsDragInput = ({
y: number;
} | null = null;
let stateAtStart: ExcalidrawElement | null = null;
let stateAtStart: ExcalidrawElement[] | null = null;
let accumulatedChange: number | null = null;
@ -72,7 +72,9 @@ const StatsDragInput = ({
const onPointerMove = (event: PointerEvent) => {
if (!stateAtStart) {
stateAtStart = deepCopyElement(element);
stateAtStart = elements.map((element) =>
deepCopyElement(element),
);
}
if (!accumulatedChange) {
@ -146,11 +148,12 @@ const StatsDragInput = ({
dragInputCallback(
0,
0,
element,
elements,
shouldKeepAspectRatio!!,
false,
v,
);
eventTarget.blur();
}
}
}}

@ -22,40 +22,43 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => {
shouldChangeByStepSize,
nextValue,
) => {
if (nextValue) {
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
const _stateAtStart = stateAtStart[0];
if (_stateAtStart) {
if (nextValue) {
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
const newElement = {
...element,
fontSize: nextFontSize,
};
const updates = refreshTextDimensions(newElement, null, elementsMap);
mutateElement(element, {
...updates,
fontSize: nextFontSize,
});
return;
}
const newElement = {
...element,
fontSize: nextFontSize,
};
const updates = refreshTextDimensions(newElement, null, elementsMap);
mutateElement(element, {
...updates,
fontSize: nextFontSize,
});
return;
}
if (stateAtStart && stateAtStart.type === "text") {
const originalFontSize = Math.round(stateAtStart.fontSize);
const changeInFontSize = Math.round(accumulatedChange);
let nextFontSize = Math.max(
originalFontSize + changeInFontSize,
MIN_FONT_SIZE,
);
if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
if (_stateAtStart.type === "text") {
const originalFontSize = Math.round(_stateAtStart.fontSize);
const changeInFontSize = Math.round(accumulatedChange);
let nextFontSize = Math.max(
originalFontSize + changeInFontSize,
MIN_FONT_SIZE,
);
if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
}
const newElement = {
...element,
fontSize: nextFontSize,
};
const updates = refreshTextDimensions(newElement, null, elementsMap);
mutateElement(element, {
...updates,
fontSize: nextFontSize,
});
}
const newElement = {
...element,
fontSize: nextFontSize,
};
const updates = refreshTextDimensions(newElement, null, elementsMap);
mutateElement(element, {
...updates,
fontSize: nextFontSize,
});
}
};
@ -63,7 +66,7 @@ const FontSize = ({ element, elementsMap }: FontSizeProps) => {
<StatsDragInput
label="F"
value={Math.round(element.fontSize * 10) / 10}
element={element}
elements={[element]}
dragInputCallback={handleFontSizeChange}
/>
);

@ -0,0 +1,143 @@
import { getCommonBounds } from "../../element";
import { mutateElement } from "../../element/mutateElement";
import type { ExcalidrawElement } from "../../element/types";
import DragInput from "./DragInput";
import type { DragInputCallbackType } from "./DragInput";
import { getStepSizedValue } from "./utils";
interface MultiDimensionProps {
property: "width" | "height";
elements: ExcalidrawElement[];
}
const STEP_SIZE = 10;
const MultiDimension = ({ property, elements }: MultiDimensionProps) => {
const handleDimensionChange: DragInputCallbackType = (
accumulatedChange,
instantChange,
stateAtStart,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
) => {
const [x1, y1, x2, y2] = getCommonBounds(stateAtStart);
const initialWidth = x2 - x1;
const initialHeight = y2 - y1;
const keepAspectRatio = true;
const aspectRatio = initialWidth / initialHeight;
if (nextValue !== undefined) {
const nextHeight =
property === "height" ? nextValue : nextValue / aspectRatio;
const scale = nextHeight / initialHeight;
const anchorX = property === "width" ? x1 : x1 + width / 2;
const anchorY = property === "height" ? y1 : y1 + height / 2;
let i = 0;
while (i < stateAtStart.length) {
const element = 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) {
const offsetX = origElement.x - anchorX;
const offsetY = origElement.y - anchorY;
const nextWidth = origElement.width * scale;
const nextHeight = origElement.height * scale;
const x = anchorX + offsetX * scale;
const y = anchorY + offsetY * scale;
mutateElement(
element,
{
width: nextWidth,
height: nextHeight,
x,
y,
},
i === stateAtStart.length - 1,
);
}
i++;
}
return;
}
const changeInWidth = property === "width" ? accumulatedChange : 0;
const changeInHeight = property === "height" ? accumulatedChange : 0;
let nextWidth = Math.max(0, initialWidth + changeInWidth);
if (property === "width") {
if (shouldChangeByStepSize) {
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
} else {
nextWidth = Math.round(nextWidth);
}
}
let nextHeight = Math.max(0, initialHeight + changeInHeight);
if (property === "height") {
if (shouldChangeByStepSize) {
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
} else {
nextHeight = Math.round(nextHeight);
}
}
if (keepAspectRatio) {
if (property === "width") {
nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100;
} else {
nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
}
}
const scale = nextHeight / initialHeight;
const anchorX = property === "width" ? x1 : x1 + width / 2;
const anchorY = property === "height" ? y1 : y1 + height / 2;
let i = 0;
while (i < stateAtStart.length) {
const element = elements[i];
const origElement = stateAtStart[i];
const offsetX = origElement.x - anchorX;
const offsetY = origElement.y - anchorY;
const nextWidth = origElement.width * scale;
const nextHeight = origElement.height * scale;
const x = anchorX + offsetX * scale;
const y = anchorY + offsetY * scale;
mutateElement(
element,
{
width: nextWidth,
height: nextHeight,
x,
y,
},
i === stateAtStart.length - 1,
);
i++;
}
};
const [x1, y1, x2, y2] = getCommonBounds(elements);
const width = x2 - x1;
const height = y2 - y1;
return (
<DragInput
label={property === "width" ? "W" : "H"}
elements={elements}
dragInputCallback={handleDimensionChange}
value={Math.round((property === "width" ? width : height) * 100) / 100}
/>
);
};
export default MultiDimension;

@ -23,6 +23,14 @@
margin-bottom: 8px;
}
.elementsCount {
width: 100%;
font-size: 12px;
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.statsItem {
width: 100%;
margin-bottom: 4px;

@ -13,6 +13,8 @@ import Angle from "./Angle";
import "./index.scss";
import FontSize from "./FontSize";
import MultiDimension from "./MultiDimension";
import { elementsAreInSameGroup } from "../../groups";
interface StatsProps {
appState: AppState;
@ -32,6 +34,9 @@ export const Stats = (props: StatsProps) => {
const singleElement =
selectedElements.length === 1 ? selectedElements[0] : null;
const multipleElements =
selectedElements.length > 1 ? selectedElements : null;
const [sceneDimension, setSceneDimension] = useState<{
width: number;
height: number;
@ -91,7 +96,7 @@ export const Stats = (props: StatsProps) => {
</table>
</div>
{singleElement && (
{selectedElements.length > 0 && (
<div
className="section"
style={{
@ -100,22 +105,51 @@ export const Stats = (props: StatsProps) => {
>
<h3>{t("stats.elementStats")}</h3>
<div className="sectionContent">
<div className="elementType">
{t(`element.${singleElement.type}`)}
{singleElement && (
<div className="sectionContent">
<div className="elementType">
{t(`element.${singleElement.type}`)}
</div>
<div className="statsItem">
<Dimension property="width" element={singleElement} />
<Dimension property="height" element={singleElement} />
<Angle element={singleElement} />
{singleElement.type === "text" && (
<FontSize
element={singleElement}
elementsMap={elementsMap}
/>
)}
</div>
{singleElement.type === "text" && <div></div>}
</div>
)}
<div className="statsItem">
<Dimension property="width" element={singleElement} />
<Dimension property="height" element={singleElement} />
<Angle element={singleElement} />
{singleElement.type === "text" && (
<FontSize element={singleElement} elementsMap={elementsMap} />
{multipleElements && (
<div className="sectionContent">
{elementsAreInSameGroup(multipleElements) && (
<div className="elementType">{t("element.group")}</div>
)}
</div>
{singleElement.type === "text" && <div></div>}
</div>
<div className="elementsCount">
<div>{t("stats.elements")}</div>
<div>{selectedElements.length}</div>
</div>
<div className="statsItem">
<MultiDimension
property="width"
elements={multipleElements}
/>
<MultiDimension
property="height"
elements={multipleElements}
/>
</div>
</div>
)}
</div>
)}
</Island>

Loading…
Cancel
Save