From 0fac3977b3a4db3bd60fdea18421de43f3245abe Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Fri, 24 Oct 2025 00:03:20 +0200 Subject: [PATCH] fewer unnecessary re-renders --- src/App/util.ts | 28 +++++++++ src/VisualEditor/VisualEditor.tsx | 101 ++++++++++++++++++++---------- 2 files changed, 95 insertions(+), 34 deletions(-) diff --git a/src/App/util.ts b/src/App/util.ts index b644b12..8ced141 100644 --- a/src/App/util.ts +++ b/src/App/util.ts @@ -38,3 +38,31 @@ export function arraysEqual(a: T[], b: T[], cmp: (a: T, b: T) => boolean = (a return true; } + +export function setsEqual(a: Set, b: Set): boolean { + if (a === b) + return true; + + if (a.size !== b.size) + return false; + + for (const itemA of a) + if (!b.has(itemA)) + return false; + + return true; +} + +export function objectsEqual(a: {[key: string]: T}, b: {[key: string]: T}, cmp: (a: T, b: T) => boolean = (a,b)=>a===b): boolean { + if (a === b) + return true; + + if (Object.keys(a).length !== Object.keys(b).length) + return false; + + for (const [keyA, valueA] of Object.entries(a)) + if (!cmp(b[keyA], valueA)) + return false; + + return true; +} diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index d56751d..f70ae2e 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -15,6 +15,8 @@ import { Connections, detectConnections } from "../statecharts/detect_connection import "./VisualEditor.css"; import { TraceState } from "@/App/App"; +import { Mode } from "@/statecharts/runtime_types"; +import { arraysEqual, objectsEqual, setsEqual } from "@/App/util"; export type VisualEditorState = { rountangles: Rountangle[]; @@ -701,29 +703,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, {(rootErrors.length>0) && {rootErrors.join(' ')}} - {state.rountangles.map(rountangle => { - return r.uid === rountangle.uid)?.parts as RectSide[] || []} - highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RectSide[]]} - error={errors - .filter(({shapeUid}) => shapeUid === rountangle.uid) - .map(({message}) => message).join(', ')} - active={highlightActive.has(rountangle.uid)} - />})} - - {state.diamonds.map(diamond => <> - r.uid === diamond.uid)?.parts as RectSide[] || []} - highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RectSide[]]} - error={errors - .filter(({shapeUid}) => shapeUid === diamond.uid) - .map(({message}) => message).join(', ')} - active={false}/> - )} + + {state.history.map(history => <> { - return txt.uid === shapeUid)} - text={txt} - selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)} - highlight={textsToHighlight.hasOwnProperty(txt.uid)} - onEdit={onEditText} - setModal={setModal} - /> - })} + {selectingState && } ; @@ -782,6 +753,68 @@ export function rountangleMinSize(size: Vec2D): Vec2D { }; } +const Rountangles = memo(function Rountangles({rountangles, selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}: {rountangles: Rountangle[], selection: Selection, sidesToHighlight: {[key: string]: RectSide[]}, rountanglesToHighlight: {[key: string]: boolean}, errors: TraceableError[], highlightActive: Mode}) { + return <>{rountangles.map(rountangle => { + return r.uid === rountangle.uid)?.parts as RectSide[] || []} + highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RectSide[]]} + error={errors + .filter(({shapeUid}) => shapeUid === rountangle.uid) + .map(({message}) => message).join(', ')} + active={highlightActive.has(rountangle.uid)} + />})}; +}, (p, n) => { + return arraysEqual(p.rountangles, n.rountangles) + && arraysEqual(p.selection, n.selection) + && objectsEqual(p.sidesToHighlight, n.sidesToHighlight) + && objectsEqual(p.rountanglesToHighlight, n.rountanglesToHighlight) + && arraysEqual(p.errors, n.errors) + && setsEqual(p.highlightActive, n.highlightActive); +}); + +const Diamonds = memo(function Diamonds({diamonds, selection, sidesToHighlight, rountanglesToHighlight, errors}: {diamonds: Diamond[], selection: Selection, sidesToHighlight: {[key: string]: RectSide[]}, rountanglesToHighlight: {[key: string]: boolean}, errors: TraceableError[]}) { + return <>{diamonds.map(diamond => <> + r.uid === diamond.uid)?.parts as RectSide[] || []} + highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RectSide[]]} + error={errors + .filter(({shapeUid}) => shapeUid === diamond.uid) + .map(({message}) => message).join(', ')} + active={false}/> + )}; +}, (p, n) => { + return arraysEqual(p.diamonds, n.diamonds) + && arraysEqual(p.selection, n.selection) + && objectsEqual(p.sidesToHighlight, n.sidesToHighlight) + && objectsEqual(p.rountanglesToHighlight, n.rountanglesToHighlight) + && arraysEqual(p.errors, n.errors); +}); + +const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch>}) { + return <>{texts.map(txt => { + return txt.uid === shapeUid)} + text={txt} + selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)} + highlight={textsToHighlight.hasOwnProperty(txt.uid)} + onEdit={onEditText} + setModal={setModal} + /> + })}; +}, (p, n) => { + return arraysEqual(p.texts, n.texts) + && arraysEqual(p.selection, n.selection) + && objectsEqual(p.textsToHighlight, n.textsToHighlight) + && arraysEqual(p.errors, n.errors) + && p.onEditText === n.onEditText + && p.setModal === n.setModal; +}); + export function Selecting(props: SelectingState) { const normalizedRect = normalizeRect(props!); return