From 2ca2ba5d1b3c2cb9123afcc9edba4da87e3c8cf5 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Thu, 23 Oct 2025 21:47:39 +0200 Subject: [PATCH] prevent unnecessary re-rendering of rountangles and diamonds --- src/App/util.ts | 15 +++++++++++++++ src/VisualEditor/DiamondSVG.tsx | 12 ++++++++++-- src/VisualEditor/RectHelpers.tsx | 5 +++-- src/VisualEditor/RountangleSVG.tsx | 17 +++++++++++++---- src/VisualEditor/VisualEditor.tsx | 8 ++++---- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/App/util.ts b/src/App/util.ts index 6babea1..b644b12 100644 --- a/src/App/util.ts +++ b/src/App/util.ts @@ -23,3 +23,18 @@ export function memoize(fn: (i: InType) => OutType) { return result; } } + +// compare arrays by value +export function arraysEqual(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; }); -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 ; +}, (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 }); diff --git a/src/VisualEditor/RectHelpers.tsx b/src/VisualEditor/RectHelpers.tsx index 6272c5c..a88d90e 100644 --- a/src/VisualEditor/RectHelpers.tsx +++ b/src/VisualEditor/RectHelpers.tsx @@ -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]) => @@ -54,4 +55,4 @@ export const RectHelper = memo(function RectHelper(props: { uid: string, size: V data-uid={props.uid} data-parts="bottom left" /> ; -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/src/VisualEditor/RountangleSVG.tsx b/src/VisualEditor/RountangleSVG.tsx index 278ee14..463f713 100644 --- a/src/VisualEditor/RountangleSVG.tsx +++ b/src/VisualEditor/RountangleSVG.tsx @@ -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 {props.rountangle.uid} - {(props.errors.length > 0) && - {props.errors.join(' ')}} + {props.error && + {props.error}} ; +}, (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 }) diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index fe367ef..f41abf2 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -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}/> )}