From 206cda78af7bee0d3f2b861ee8990a9adb590bd8 Mon Sep 17 00:00:00 2001 From: David Montero Date: Sat, 18 Jul 2026 19:18:27 +0200 Subject: [PATCH] fix(components): type-coerce string properties + reseat on pininfo-change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the 'digits=4 display seated with the 1-digit COM pinout' bug: property values arrive as STRINGS (agent set_component_property, the property dialog's text inputs) and were assigned to the web component verbatim — wokwi's 7segment does switch(this.digits) with numeric cases, so el.digits='4' silently fell back to the 1-digit pinout (and 'false' stayed truthy for boolean props like colon). - DynamicComponent now coerces string values to the TYPE of the metadata default for that key (number/boolean) before assigning. - New pininfo-change listener: when a property swaps the element's pin set (digits, flip, pins edge), the elements announce it — re-derive the breadboard seating then, with the fresh pinout, instead of never. --- frontend/src/components/DynamicComponent.tsx | 41 +++++++++++++++++++- frontend/src/store/useSimulatorStore.ts | 7 ++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index 86171d42..8e0a6f1d 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -293,20 +293,57 @@ export const DynamicComponent: React.FC = ({ const isInteractive = logic?.attachEvents !== undefined; /** - * Sync React properties to Web Component + * Sync React properties to Web Component. + * + * Values arriving as strings (agent set_component_property, the text + * inputs in the property dialog) are coerced to the type of the + * metadata DEFAULT for that key. Without this, `el.digits = '4'` + * (string) silently breaks wokwi elements that strict-match + * (`switch (this.digits) { case 4: ... }` -> falls back to the 1-digit + * pinout), and `'false'` stays truthy for boolean props like colon. */ useEffect(() => { if (!elementRef.current) return; Object.entries(properties).forEach(([key, value]) => { try { - (elementRef.current as any)[key] = value; + let coerced: any = value; + if (typeof value === 'string') { + const def = metadata.defaultValues?.[key]; + if (typeof def === 'number' && value.trim() !== '' && !Number.isNaN(Number(value))) { + coerced = Number(value); + } else if (typeof def === 'boolean') { + coerced = value === 'true' || value === '1'; + } + } + (elementRef.current as any)[key] = coerced; } catch (error) { console.warn(`Failed to set property ${key} on ${metadata.tagName}:`, error); } }); }, [properties, metadata.tagName]); + /** + * Property changes that swap the element's pin SET (7segment digits, + * LED flip, display pins edge) re-render asynchronously and announce + * themselves with a 'pininfo-change' event. Re-derive the breadboard + * seating then — reseating synchronously on the property write would + * read the STALE pinout and seat ghost pins. + */ + useEffect(() => { + const el = elementRef.current; + if (!el) return; + const onPinInfoChange = () => { + try { + useSimulatorStore.getState().reseatComponentOnBreadboard(id); + } catch { + // headless / tests + } + }; + el.addEventListener('pininfo-change', onPinInfoChange); + return () => el.removeEventListener('pininfo-change', onPinInfoChange); + }, [id, metadata.tagName]); + /** * Extract pinInfo from web component after it initializes */ diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 6d5b0dc5..fa340088 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -2479,6 +2479,13 @@ export const useSimulatorStore = create((set, get) => { updates.properties && 'rotation' in updates.properties; if (updates.x !== undefined || updates.y !== undefined || rotationChanged) { get().updateWirePositions(id); + // Reseat ONLY on geometry changes (move/rotate) — the DOM pinInfo is + // still valid for those. Property changes that swap the pin SET + // (7segment digits, LED flip) re-render asynchronously; reseating + // now would read the STALE pinout and seat ghost pins (seen live: + // a digits=4 display seated with the 1-digit COM pinout). Those go + // through the element's 'pininfo-change' event instead + // (DynamicComponent listener). get().reseatComponentOnBreadboard(id); }