prevent unnecessary re-rendering of rountangles and diamonds

This commit is contained in:
Joeri Exelmans 2025-10-23 21:47:39 +02:00
parent 0fc3775a11
commit 2ca2ba5d1b
5 changed files with 45 additions and 12 deletions

View file

@ -23,3 +23,18 @@ export function memoize<InType,OutType>(fn: (i: InType) => OutType) {
return result;
}
}
// compare arrays by value
export function arraysEqual<T>(a: T[], b: T[], cmp: (a: T, b: T) => boolean = (a,b)=>a===b): boolean {
if (a === b)
return true;
if (a.length !== b.length)
return false;
for (let i=0; i<a.length; i++)
if (!cmp(a[i],b[i]))
return false;
return true;
}

View file

@ -3,6 +3,7 @@ import { rountangleMinSize } from "./VisualEditor";
import { Vec2D } from "./geometry";
import { RectHelper } from "./RectHelpers";
import { memo } from "react";
import { arraysEqual } from "@/App/util";
export const DiamondShape = memo(function DiamondShape(props: {size: Vec2D, extraAttrs: object}) {
const minSize = rountangleMinSize(props.size);
@ -20,12 +21,13 @@ export const DiamondShape = memo(function DiamondShape(props: {size: Vec2D, extr
/>;
});
export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: RountanglePart[]; highlight: RountanglePart[]; error?: string; active: boolean; }) {
console.log('render diamond', props.diamond.uid);
const minSize = rountangleMinSize(props.diamond.size);
const extraAttrs = {
className: ''
+ (props.selected.length === 4 ? " selected" : "")
+ (props.errors.length > 0 ? " error" : "")
+ (props.error ? " error" : "")
+ (props.active ? " active" : ""),
"data-uid": props.diamond.uid,
"data-parts": "left top right bottom",
@ -39,4 +41,10 @@ export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; se
<RectHelper uid={props.diamond.uid} size={minSize} highlight={props.highlight} selected={props.selected} />
</g>;
}, (prevProps, nextProps) => {
return prevProps.diamond === nextProps.diamond
&& arraysEqual(prevProps.selected, nextProps.selected)
&& arraysEqual(prevProps.highlight, nextProps.highlight)
&& prevProps.error === nextProps.error
&& prevProps.active === nextProps.active
});

View file

@ -12,7 +12,8 @@ function lineGeometryProps(size: Vec2D): [RountanglePart, object][] {
];
}
export const RectHelper = memo(function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) {
// no need to memo() this component, the parent component is already memoized
export const RectHelper = function RectHelper(props: { uid: string, size: Vec2D, selected: RountanglePart[], highlight: string[] }) {
const geomProps = lineGeometryProps(props.size);
return <>
{geomProps.map(([side, ps]) => <g key={side}>
@ -54,4 +55,4 @@ export const RectHelper = memo(function RectHelper(props: { uid: string, size: V
data-uid={props.uid}
data-parts="bottom left" />
</>;
});
};

View file

@ -3,9 +3,12 @@ import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax";
import { ROUNTANGLE_RADIUS } from "./parameters";
import { RectHelper } from "./RectHelpers";
import { rountangleMinSize } from "./VisualEditor";
import { arraysEqual } from "@/App/util";
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RountanglePart[]; highlight: RountanglePart[]; error?: string; active: boolean; }) {
console.log('render rountangle', props.rountangle.uid);
const { topLeft, size, uid } = props.rountangle;
// always draw a rountangle with a minimum size
// during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it
@ -14,7 +17,7 @@ export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rou
className: 'rountangle'
+ (props.selected.length === 4 ? " selected" : "")
+ (' ' + props.rountangle.kind)
+ (props.errors.length > 0 ? " error" : "")
+ (props.error ? " error" : "")
+ (props.active ? " active" : ""),
"data-uid": uid,
"data-parts": "left top right bottom",
@ -31,11 +34,17 @@ export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rou
<text x={10} y={20} className="uid">{props.rountangle.uid}</text>
{(props.errors.length > 0) &&
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.errors.join(' ')}</text>}
{props.error &&
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.error}</text>}
<RectHelper uid={uid} size={minSize}
selected={props.selected}
highlight={props.highlight} />
</g>;
}, (prevProps, nextProps) => {
return prevProps.rountangle === nextProps.rountangle
&& arraysEqual(prevProps.selected, nextProps.selected)
&& arraysEqual(prevProps.highlight, nextProps.highlight)
&& prevProps.error === nextProps.error
&& prevProps.active === nextProps.active
})

View file

@ -718,9 +718,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
rountangle={rountangle}
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
errors={errors
error={errors
.filter(({shapeUid}) => shapeUid === rountangle.uid)
.map(({message}) => message)}
.map(({message}) => message).join(', ')}
active={highlightActive.has(rountangle.uid)}
/>})}
@ -730,9 +730,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
diamond={diamond}
selected={selection.find(r => r.uid === diamond.uid)?.parts || []}
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
errors={errors
error={errors
.filter(({shapeUid}) => shapeUid === diamond.uid)
.map(({message}) => message)}
.map(({message}) => message).join(', ')}
active={false}/>
</>)}