throttled stats
This commit is contained in:
parent
dbc48cfee2
commit
80b9fd18b9
@ -2,6 +2,7 @@
|
||||
|
||||
.excalidraw {
|
||||
.Stats {
|
||||
width: 202px;
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
|
@ -1,16 +1,23 @@
|
||||
import React from "react";
|
||||
import { nanoid } from "nanoid";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { getCommonBounds } from "../element/bounds";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { KEYS } from "../keys";
|
||||
import { degreeToRadian, radianToDegree } from "../math";
|
||||
import { getTargetElements } from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { CloseIcon } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import "./Stats.scss";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
const STATS_TIMEOUT = 50;
|
||||
interface StatsProps {
|
||||
appState: AppState;
|
||||
scene: Scene;
|
||||
@ -19,48 +26,114 @@ interface StatsProps {
|
||||
renderCustomStats: ExcalidrawProps["renderCustomStats"];
|
||||
}
|
||||
|
||||
type ElementStatItem = {
|
||||
label: string;
|
||||
value: number;
|
||||
element: NonDeletedExcalidrawElement;
|
||||
version: string;
|
||||
property: "x" | "y" | "width" | "height" | "angle";
|
||||
};
|
||||
|
||||
export const Stats = (props: StatsProps) => {
|
||||
const elements = props.scene.getNonDeletedElements();
|
||||
const boundingBox = getCommonBounds(elements);
|
||||
const selectedElements = getTargetElements(elements, props.appState);
|
||||
const selectedBoundingBox = getCommonBounds(selectedElements);
|
||||
|
||||
const stats =
|
||||
selectedElements.length === 1
|
||||
const singleElement =
|
||||
selectedElements.length === 1 ? selectedElements[0] : null;
|
||||
|
||||
const [sceneDimension, setSceneDimension] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const throttledSetSceneDimension = useMemo(
|
||||
() =>
|
||||
throttle((elements: readonly NonDeletedExcalidrawElement[]) => {
|
||||
const boundingBox = getCommonBounds(elements);
|
||||
setSceneDimension({
|
||||
width: Math.round(boundingBox[2]) - Math.round(boundingBox[0]),
|
||||
height: Math.round(boundingBox[3]) - Math.round(boundingBox[1]),
|
||||
});
|
||||
}, STATS_TIMEOUT),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
throttledSetSceneDimension(elements);
|
||||
}, [props.scene.version, elements, throttledSetSceneDimension]);
|
||||
|
||||
useEffect(
|
||||
() => () => throttledSetSceneDimension.cancel(),
|
||||
[throttledSetSceneDimension],
|
||||
);
|
||||
|
||||
const [elementStats, setElementStats] = useState<ElementStatItem[]>([]);
|
||||
|
||||
const throttledSetElementStats = useMemo(
|
||||
() =>
|
||||
throttle((element: NonDeletedExcalidrawElement | null) => {
|
||||
const stats: ElementStatItem[] = element
|
||||
? [
|
||||
{
|
||||
label: "X",
|
||||
value: Math.round(selectedBoundingBox[0]),
|
||||
element: selectedElements[0],
|
||||
value: Math.round(element.x),
|
||||
element,
|
||||
property: "x",
|
||||
version: nanoid(),
|
||||
},
|
||||
{
|
||||
label: "Y",
|
||||
value: Math.round(selectedBoundingBox[1]),
|
||||
element: selectedElements[0],
|
||||
value: Math.round(element.y),
|
||||
element,
|
||||
property: "y",
|
||||
version: nanoid(),
|
||||
},
|
||||
{
|
||||
label: "W",
|
||||
value: Math.round(selectedBoundingBox[2] - selectedBoundingBox[0]),
|
||||
element: selectedElements[0],
|
||||
value: Math.round(element.width),
|
||||
element,
|
||||
property: "width",
|
||||
version: nanoid(),
|
||||
},
|
||||
{
|
||||
label: "H",
|
||||
value: Math.round(selectedBoundingBox[3] - selectedBoundingBox[1]),
|
||||
element: selectedElements[0],
|
||||
value: Math.round(element.height),
|
||||
element,
|
||||
property: "height",
|
||||
version: nanoid(),
|
||||
},
|
||||
{
|
||||
label: "A",
|
||||
value: selectedElements[0].angle,
|
||||
element: selectedElements[0],
|
||||
value: Math.round(radianToDegree(element.angle) * 100) / 100,
|
||||
element,
|
||||
property: "angle",
|
||||
version: nanoid(),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
setElementStats(stats);
|
||||
}, STATS_TIMEOUT),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
throttledSetElementStats(singleElement);
|
||||
}, [
|
||||
singleElement,
|
||||
singleElement?.version,
|
||||
singleElement?.versionNonce,
|
||||
throttledSetElementStats,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() => () => throttledSetElementStats.cancel(),
|
||||
[throttledSetElementStats],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="Stats">
|
||||
<Island padding={3}>
|
||||
@ -80,31 +153,30 @@ export const Stats = (props: StatsProps) => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("stats.width")}</td>
|
||||
<td>
|
||||
{Math.round(boundingBox[2]) - Math.round(boundingBox[0])}
|
||||
</td>
|
||||
<td>{sceneDimension.width}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t("stats.height")}</td>
|
||||
<td>
|
||||
{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}
|
||||
</td>
|
||||
<td>{sceneDimension.height}</td>
|
||||
</tr>
|
||||
{props.renderCustomStats?.(elements, props.appState)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{selectedElements.length > 0 && (
|
||||
<div className="section">
|
||||
{singleElement && (
|
||||
<div
|
||||
className="section"
|
||||
style={{
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
<h3>{t("stats.elementStats")}</h3>
|
||||
|
||||
<div className="sectionContent">
|
||||
{selectedElements.length === 1 && (
|
||||
<div className="elementType">
|
||||
{t(`element.${selectedElements[0].type}`)}
|
||||
{t(`element.${singleElement.type}`)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
@ -113,7 +185,8 @@ export const Stats = (props: StatsProps) => {
|
||||
gap: "4px 8px",
|
||||
}}
|
||||
>
|
||||
{stats.map((statsItem) => (
|
||||
{elementStats.map((statsItem) => {
|
||||
return (
|
||||
<label
|
||||
className="color-input-container"
|
||||
key={statsItem.property}
|
||||
@ -128,7 +201,7 @@ export const Stats = (props: StatsProps) => {
|
||||
</div>
|
||||
<input
|
||||
id={statsItem.label}
|
||||
key={statsItem.value}
|
||||
key={statsItem.version}
|
||||
defaultValue={statsItem.value}
|
||||
className="color-picker-input"
|
||||
style={{
|
||||
@ -137,14 +210,21 @@ export const Stats = (props: StatsProps) => {
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
onKeyDown={(event) => {
|
||||
const value = Number(event.target.value);
|
||||
let value = Number(event.target.value);
|
||||
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
value =
|
||||
statsItem.property === "angle"
|
||||
? degreeToRadian(value)
|
||||
: value;
|
||||
|
||||
if (event.key === KEYS.ENTER) {
|
||||
if (!isNaN(value)) {
|
||||
mutateElement(statsItem.element, {
|
||||
[statsItem.property]: value,
|
||||
});
|
||||
}
|
||||
|
||||
event.target.value = statsItem.element[
|
||||
statsItem.property as keyof ExcalidrawElement
|
||||
@ -152,7 +232,16 @@ export const Stats = (props: StatsProps) => {
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
const value = Number(event.target.value);
|
||||
let value = Number(event.target.value);
|
||||
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
value =
|
||||
statsItem.property === "angle"
|
||||
? degreeToRadian(value)
|
||||
: value;
|
||||
|
||||
if (!isNaN(value)) {
|
||||
mutateElement(statsItem.element, {
|
||||
@ -166,7 +255,8 @@ export const Stats = (props: StatsProps) => {
|
||||
}}
|
||||
></input>
|
||||
</label>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -245,7 +245,7 @@
|
||||
"ellipse": "Ellipse",
|
||||
"arrow": "Arrow",
|
||||
"line": "Line",
|
||||
"freeDraw": "Freedraw",
|
||||
"freedraw": "Freedraw",
|
||||
"text": "Text",
|
||||
"image": "Image",
|
||||
"group": "Group"
|
||||
|
@ -472,3 +472,11 @@ export const isRightAngle = (angle: number) => {
|
||||
// angle, which we can check with modulo after rounding.
|
||||
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
|
||||
};
|
||||
|
||||
export const radianToDegree = (r: number) => {
|
||||
return (r * 180) / Math.PI;
|
||||
};
|
||||
|
||||
export const degreeToRadian = (d: number) => {
|
||||
return (d / 180) * Math.PI;
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
} from "../element/types";
|
||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
@ -56,6 +57,7 @@ class Scene {
|
||||
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
||||
private elements: readonly ExcalidrawElement[] = [];
|
||||
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
||||
version: string = nanoid();
|
||||
|
||||
getElementsIncludingDeleted() {
|
||||
return this.elements;
|
||||
@ -120,6 +122,10 @@ class Scene {
|
||||
}
|
||||
|
||||
informMutation() {
|
||||
// we update the version of the scene when we want to inform callbacks of
|
||||
// changes to the scene
|
||||
this.version = nanoid();
|
||||
|
||||
for (const callback of Array.from(this.callbacks)) {
|
||||
callback();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user