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