From b2cb93fde589cc862f6fe3bfeece534d7f8c5b96 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 9 Mar 2026 13:06:46 -0400 Subject: [PATCH 1/2] fix: resolve FBD editor infinite render loop causing high CPU usage Strip hasDivergence UI property from store comparisons to prevent isEqual mismatch (undefined vs false) that triggered continuous re-renders. Use selective Zustand selectors and memoize divergence computation in FbdEditor wrapper. Co-Authored-By: Claude Opus 4.6 --- .../editor/graphical/FBD/index.tsx | 71 +++++++++---------- .../_molecules/graphical-editor/fbd/index.tsx | 25 ++++--- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/renderer/components/_features/[workspace]/editor/graphical/FBD/index.tsx b/src/renderer/components/_features/[workspace]/editor/graphical/FBD/index.tsx index b4ee2fd4a..7f6f1c8ab 100644 --- a/src/renderer/components/_features/[workspace]/editor/graphical/FBD/index.tsx +++ b/src/renderer/components/_features/[workspace]/editor/graphical/FBD/index.tsx @@ -3,48 +3,24 @@ import { BlockVariant } from '@root/renderer/components/_atoms/graphical-editor/ import { FBDBody } from '@root/renderer/components/_molecules/graphical-editor/fbd' import { useOpenPLCStore } from '@root/renderer/store' import { zodFBDFlowSchema } from '@root/renderer/store/slices' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' export default function FbdEditor() { - const { - editor, - fbdFlows, - project: { - data: { pous }, - }, - libraries: { user: userLibraries }, - fbdFlowActions, - projectActions: { updatePou }, - sharedWorkspaceActions: { handleFileAndWorkspaceSavedState }, - workspace: { isDebuggerVisible }, - } = useOpenPLCStore() + const editor = useOpenPLCStore((state) => state.editor) + const fbdFlows = useOpenPLCStore((state) => state.fbdFlows) + const pous = useOpenPLCStore((state) => state.project.data.pous) + const userLibraries = useOpenPLCStore((state) => state.libraries.user) + const fbdFlowActions = useOpenPLCStore((state) => state.fbdFlowActions) + const updatePou = useOpenPLCStore((state) => state.projectActions.updatePou) + const handleFileAndWorkspaceSavedState = useOpenPLCStore( + (state) => state.sharedWorkspaceActions.handleFileAndWorkspaceSavedState, + ) + const isDebuggerVisible = useOpenPLCStore((state) => state.workspace.isDebuggerVisible) const flow = fbdFlows.find((flow) => flow.name === editor.meta.name) const flowUpdated = flow?.updated || false - const nodeDivergences = getLibraryDivergences() - - /** - * Update the flow state to project JSON - */ - useEffect(() => { - if (!flowUpdated) return - - const flowSchema = zodFBDFlowSchema.safeParse(flow) - if (!flowSchema.success) return - - updatePou({ - name: editor.meta.name, - content: { - language: 'fbd', - value: flowSchema.data, - }, - }) - - fbdFlowActions.setFlowUpdated({ editorName: editor.meta.name, updated: false }) - handleFileAndWorkspaceSavedState(editor.meta.name) - }, [flowUpdated]) - function getLibraryDivergences() { + const nodeDivergences = useMemo(() => { if (!flow) return [] const divergences = [] @@ -96,7 +72,28 @@ export default function FbdEditor() { } return divergences - } + }, [flow?.rung.nodes, userLibraries, pous]) + + /** + * Update the flow state to project JSON + */ + useEffect(() => { + if (!flowUpdated) return + + const flowSchema = zodFBDFlowSchema.safeParse(flow) + if (!flowSchema.success) return + + updatePou({ + name: editor.meta.name, + content: { + language: 'fbd', + value: flowSchema.data, + }, + }) + + fbdFlowActions.setFlowUpdated({ editorName: editor.meta.name, updated: false }) + handleFileAndWorkspaceSavedState(editor.meta.name) + }, [flowUpdated]) return (
diff --git a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx index 70fe2ac1c..e29384bbd 100644 --- a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx +++ b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx @@ -221,8 +221,8 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false } debugVariableValues, debugForcedVariables, editor.meta.name, - project, - getCompositeKey, + pouRef?.data.variables, + project.data.configuration.resource.instances, ]) const styledNodes = useMemo(() => { @@ -263,25 +263,32 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false } } const updateRungState = () => { + const stripDivergence = (node: FlowNode) => { + const { hasDivergence: _hd, ...cleanData } = node.data + return { ...node, data: cleanData } + } + const rungLocalCopy = { ...rungLocal, - nodes: rungLocal.nodes.map((node) => { - const localObjectData = { ...node.data } - return { ...node, data: localObjectData } - }), + nodes: rungLocal.nodes.map(stripDivergence), + } + + const rungClean = { + ...rung, + nodes: rung.nodes.map(stripDivergence), } // Make node data mirror be the rung and not the rungLocal // This is made because the rungLocal is a local copy and may not reflect the latest changes in the store // And the store saves all the block data updates const isSelectedNodeDataEqual = - rung.selectedNodes.length > 0 - ? rung.selectedNodes.every((node) => { + rungClean.selectedNodes.length > 0 + ? rungClean.selectedNodes.every((node) => { const localNode = rungLocalCopy.nodes.find((n) => n.id === node.id) return localNode ? isEqual(localNode.data, node.data) : false }) : true - const skipUpdate = (dragging || isEqual(rungLocalCopy, rung)) && isSelectedNodeDataEqual + const skipUpdate = (dragging || isEqual(rungLocalCopy, rungClean)) && isSelectedNodeDataEqual if (skipUpdate) { return From 9f132898031f82a2a240be25a0a6e9bda6ff15e9 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 9 Mar 2026 13:33:53 -0400 Subject: [PATCH 2/2] fix: autocomplete not suggesting variables with user-defined data types Case-sensitivity mismatch in type filtering: getVariableRestrictionType returns user-defined types in original case (e.g. "Irrigation_State") but the filter lowercased only the variable side, causing the comparison to always fail. Normalize both sides with .toLowerCase(). Co-Authored-By: Claude Opus 4.6 --- .../_atoms/graphical-editor/fbd/autocomplete/index.tsx | 2 +- .../_atoms/graphical-editor/ladder/autocomplete/index.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx b/src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx index 11013763a..ff1a0d2db 100644 --- a/src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx +++ b/src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx @@ -104,7 +104,7 @@ const FBDBlockAutoComplete = forwardRef variable.name.toLowerCase().includes(valueToSearch.toLowerCase()) && (variableRestrictions.values === undefined || - variableRestrictions.values.includes(variable.type.value.toLowerCase())), + variableRestrictions.values.map((v) => v.toLowerCase()).includes(variable.type.value.toLowerCase())), ) .sort((a, b) => { const aNumber = extractNumberAtEnd(a.name).number diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx index 1b516811c..a28379b55 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx @@ -83,7 +83,12 @@ const VariablesBlockAutoComplete = forwardRef v.toLowerCase()) + .includes(variable.type.value.toLowerCase())) && (variableRestrictions.limitations === undefined || !variableRestrictions.limitations.includes(variable.type.definition)), )