From 306378f074624ea11876680427cd9e89018c986f Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 12:51:12 -0600 Subject: [PATCH 1/9] DRY - Sanitize seed helper --- src/modules/utils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/modules/utils.ts b/src/modules/utils.ts index a3e9665..d998bf2 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,3 +1,14 @@ +/** + * Sanitize a Balatro seed: uppercase, replace 0→O, strip invalid chars, max 8 chars. + */ +export function sanitizeSeed(seed: string): string { + return seed + .toUpperCase() + .replace(/0/g, 'O') + .replace(/[^A-Z1-9]/g, '') + .slice(0, 8); +} + export function getStandardCardPosition(rank: string, suit: string) { const rankMap:{ [key:string] : number } = { '2': 0, '3': 1, '4': 2, '5': 3, '6': 4, '7': 5, '8': 6, '9': 7, '10': 8, 'Jack': 9, 'Queen': 10, 'King': 11, 'Ace': 12 From 04c1a67140a462091e9213cec3ee4d59e05b9112 Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 15:56:35 -0600 Subject: [PATCH 2/9] Debounce and local state for 'antes' input Use a local state for the Max Ante input and debounce updates to the global state. Added useState/useEffect and useDebouncedCallback imports, introduced localAntes synced from antes, and a debouncedSetAntes(200ms) to avoid rapid updates. Updated NumberInput to bind to localAntes, parse string values defensively, and call the debounced setter instead of immediately mutating the global state. Cleans up previous immediate NaN/null handling and reduces unnecessary re-renders/updates. --- src/components/Rendering/canvasRenderer.tsx | 100 ++++++++------- src/components/SeedInputAutoComplete.tsx | 118 +++++++++++------- src/components/blueprint/layout/navbar.tsx | 27 ++-- .../blueprint/simpleView/simple.tsx | 15 +-- .../blueprint/standardView/index.tsx | 2 +- .../ImmolateWrapper/CardEngines/Cards.ts | 2 + src/modules/ImmolateWrapper/index.ts | 4 +- src/modules/state/analysisResultProvider.tsx | 41 ++++-- src/modules/state/downloadProvider.tsx | 9 +- src/modules/state/store.ts | 16 ++- 10 files changed, 194 insertions(+), 140 deletions(-) diff --git a/src/components/Rendering/canvasRenderer.tsx b/src/components/Rendering/canvasRenderer.tsx index a50156b..eea3ee6 100644 --- a/src/components/Rendering/canvasRenderer.tsx +++ b/src/components/Rendering/canvasRenderer.tsx @@ -2,6 +2,7 @@ import {Layer} from "../../modules/classes/Layer.ts"; import {useEffect, useRef, useState} from "react"; import {useForceUpdate, useHover, useMergedRef, useMouse, useResizeObserver} from "@mantine/hooks"; import {AspectRatio} from "@mantine/core"; +import React from "react"; const globalImageCache = new Map(); interface RenderCanvasProps { @@ -109,59 +110,64 @@ interface SimpleRenderProps { -export function SimpleRenderCanvas({ layers, invert = false }: SimpleRenderProps) { - const canvasRef = useRef(null); - const [ratio, setRatio] = useState(3 / 4); - const forceUpdate = useForceUpdate(); - useEffect(() => { - if (!canvasRef.current || !layers || layers.length === 0) return; - const canvas = canvasRef.current; - const context = canvas.getContext('2d'); - if (!context) return; +export const SimpleRenderCanvas = React.forwardRef( + ({ layers, invert = false }, ref) => { + const canvasRef = useRef(null); + const [ratio, setRatio] = useState(3 / 4); + const forceUpdate = useForceUpdate(); + useEffect(() => { + const actualRef = (ref && 'current' in ref) ? ref : canvasRef; + if (!actualRef.current || !layers || layers.length === 0) return; + const canvas = actualRef.current; + const context = canvas.getContext('2d'); + if (!context) return; context.clearRect(0, 0, canvas.width, canvas.height); - - layers - .sort((a, b) => a.order - b.order) - .forEach(layer => { - if (globalImageCache.has(layer.source)) { - const image = globalImageCache.get(layer.source) as HTMLImageElement; - const imageRatio = renderImage(canvas, context, image, layer); - if (layer.order === 0) { - setRatio(imageRatio); - } - return; - } - loadImage(layer.source) - .then((img: HTMLImageElement) => { - const imageRatio = renderImage(canvas, context, img, layer); - globalImageCache.set(layer.source, img); + context.clearRect(0, 0, canvas.width, canvas.height); + + layers + .sort((a, b) => a.order - b.order) + .forEach(layer => { + if (globalImageCache.has(layer.source)) { + const image = globalImageCache.get(layer.source) as HTMLImageElement; + const imageRatio = renderImage(canvas, context, image, layer); if (layer.order === 0) { setRatio(imageRatio); } - forceUpdate() - }) - }); - - if (invert) { - canvas.style.filter = 'invert(0.8)'; - } else { - canvas.style.filter = 'none'; - } - }, [layers, invert]); + return; + } + loadImage(layer.source) + .then((img: HTMLImageElement) => { + const imageRatio = renderImage(canvas, context, img, layer); + globalImageCache.set(layer.source, img); + if (layer.order === 0) { + setRatio(imageRatio); + } + forceUpdate() + }) + }); + + if (invert) { + canvas.style.filter = 'invert(0.8)'; + } else { + canvas.style.filter = 'none'; + } + }, [layers, invert]); + + return ( + + + + ); + } +); - return ( - - - - ); -} // Advanced card rendering with canvas diff --git a/src/components/SeedInputAutoComplete.tsx b/src/components/SeedInputAutoComplete.tsx index 37517f5..52d8c11 100644 --- a/src/components/SeedInputAutoComplete.tsx +++ b/src/components/SeedInputAutoComplete.tsx @@ -1,16 +1,44 @@ -import {Autocomplete, Button, Group, NativeSelect, Paper} from "@mantine/core"; +import React, {useState, useRef} from "react"; +import { Autocomplete, Button, Group, NativeSelect, Paper } from "@mantine/core"; +import { useDebouncedCallback } from "@mantine/hooks"; import {popularSeeds, SeedsWithLegendary} from "../modules/const.ts"; import {useCardStore} from "../modules/state/store.ts"; +import {sanitizeSeed} from "../modules/utils.ts"; +const seedAutoCompleteData = [ + { + group: 'Popular Seeds', + items: popularSeeds + }, { + group: 'Generated Seeds With Legendary Jokers', + items: SeedsWithLegendary + } +]; + +const allSuggestions = [...popularSeeds, ...SeedsWithLegendary]; + +interface SeedInputProps { + seed: string; + setSeed: (seed: string) => void; + w?: number | string; + showDeckSelect?: boolean; + label?: string; + placeholder?: string; +} + +function SeedInputAutoComplete({ seed, setSeed, w, showDeckSelect, label = 'Seed', placeholder = 'Enter Seed' }: SeedInputProps) { + const [localSeed, setLocalSeed] = useState(seed); + + const debouncedSetSeed = useDebouncedCallback((value: string) => { + setLocalSeed(sanitizeSeed(value)); + if (value) setSeed(value); + }, 200); -export function QuickAnalyze() { - const seed = useCardStore(state => state.immolateState.seed); - const setSeed = useCardStore(state => state.setSeed); const deck = useCardStore(state => state.immolateState.deck); const setDeck = useCardStore(state => state.setDeck); - const setStart = useCardStore(state => state.setStart); + const sectionWidth = 130; - const select = ( + const deckSelect = showDeckSelect ? ( Plasma Deck + ) : undefined; + + return ( + { + setLocalSeed(value); + if (allSuggestions.includes(value)) { + setSeed(value); + } else { + debouncedSetSeed(value); + } + }} + rightSection={deckSelect} + rightSectionWidth={showDeckSelect ? sectionWidth : undefined} + /> ); +} + +export function QuickAnalyze() { + const seed = useCardStore(state => state.immolateState.seed); + const setSeed = useCardStore(state => state.setSeed); + const setStart = useCardStore(state => state.setStart); + return ( - setSeed(e)} - rightSection={select} - rightSectionWidth={sectionWidth} /> - + ); - } -export default function SeedInputAutoComplete({seed, setSeed}: { seed: string, setSeed: (seed: string) => void }) { - return ( - setSeed(e)} - data={[ - { - group: 'Popular Seeds', - items: popularSeeds - }, { - group: 'Generated Seeds With Legendary Jokers', - items: SeedsWithLegendary - - } - ]} - /> - ); -} \ No newline at end of file +export default SeedInputAutoComplete; \ No newline at end of file diff --git a/src/components/blueprint/layout/navbar.tsx b/src/components/blueprint/layout/navbar.tsx index 00189ee..b063e20 100644 --- a/src/components/blueprint/layout/navbar.tsx +++ b/src/components/blueprint/layout/navbar.tsx @@ -17,7 +17,7 @@ import { useMantineColorScheme, useMantineTheme } from "@mantine/core"; -import React from "react"; +import React, {useState, useEffect} from "react"; import { IconFileText, IconJoker, @@ -32,6 +32,7 @@ import UnlocksModal from "../../unlocksModal.tsx"; import FeaturesModal from "../../FeaturesModal.tsx"; import {RerollCalculatorModal} from "../../RerollCalculatorModal.tsx"; import {GaEvent} from "../../../modules/useGA.ts"; +import { useDebouncedCallback } from "@mantine/hooks"; import SeedInputAutoComplete from "../../SeedInputAutoComplete.tsx"; import { useBlueprintTheme} from "../../../modules/state/themeProvider.tsx"; import type {KnownThemes} from "../../../modules/state/themeProvider.tsx"; @@ -71,13 +72,16 @@ export default function NavBar() { const reset = useCardStore(state => state.reset); const hasSettingsChanged = useCardStore((state) => state.applicationState.hasSettingsChanged); + const [localAntes, setLocalAntes] = useState(antes); + useEffect(() => { setLocalAntes(antes); }, [antes]); + const debouncedSetAntes = useDebouncedCallback((val: number) => { + if (val !== antes) setAntes(val); + }, 200); + const handleAnalyzeClick = () => { setStart(true); } - - - return ( @@ -155,16 +159,11 @@ export default function NavBar() { { - // Guard against null/NaN from the NumberInput - if (val === null || Number.isNaN(Number(val))) { - // keep previous value (do not set) or fallback to 1 to be defensive - // Here we fallback to 1 to avoid passing invalid values into the engine - setAntes(1); - } else { - setAntes(Math.floor(Number(val))); - } + value={localAntes} + onChange={(val: number | string) => { + const num = typeof val === 'string' ? parseInt(val) || 8 : val; + setLocalAntes(num); + debouncedSetAntes(num); }} /> (null); - const [visibleAntes, setVisibleAntes] = useState>([1]); // Start with first ante visible - const [loadingNextAnte, setLoadingNextAnte] = useState(2); // Track which ante is loading - const selectedAnte = useCardStore(state => state.applicationState.selectedAnte); - const setSelectedAnte = useCardStore(state => state.setSelectedAnte); - const debouncedSetSelectedAnte = useDebouncedCallback(setSelectedAnte, 500) + const [visibleAntes, setVisibleAntes] = useState>([1]); + const [loadingNextAnte, setLoadingNextAnte] = useState(2); const lockedCards = useCardStore(state => state.lockState.lockedCards); const clearLockedCards = useCardStore(state => state.clearLockedCards); const hasLockedCards = Object.keys(lockedCards).length > 0; @@ -373,12 +370,6 @@ function Simple() { useEffect(() => { if (entry?.isIntersecting) { - // When this ante is visible, make the next one available - const currentAnte = anteNumber; - if (currentAnte !== selectedAnte) { - debouncedSetSelectedAnte(currentAnte); - } - // Set the current ante as selected const nextAnte = anteNumber + 1; if (nextAnte <= anteEntries.length && !visibleAntes.includes(nextAnte)) { diff --git a/src/components/blueprint/standardView/index.tsx b/src/components/blueprint/standardView/index.tsx index b408167..5868696 100644 --- a/src/components/blueprint/standardView/index.tsx +++ b/src/components/blueprint/standardView/index.tsx @@ -549,7 +549,7 @@ export function Blueprint() { const outputOpened = useCardStore(state => state.applicationState.asideOpen); const download = useDownloadSeedResults() useEffect(() => { - if(typeof window !== 'undefined') { + if(typeof window !== 'undefined' && !!download) { window.saveSeedDebug = download } }, [download]); diff --git a/src/modules/ImmolateWrapper/CardEngines/Cards.ts b/src/modules/ImmolateWrapper/CardEngines/Cards.ts index 1e05a78..6c682b0 100644 --- a/src/modules/ImmolateWrapper/CardEngines/Cards.ts +++ b/src/modules/ImmolateWrapper/CardEngines/Cards.ts @@ -295,9 +295,11 @@ export class Pack { } export class SeedResultsContainer { + isLoading: boolean; antes: { [key: number]: Ante }; constructor() { this.antes = {} + this.isLoading = true; } } export interface Blind { diff --git a/src/modules/ImmolateWrapper/index.ts b/src/modules/ImmolateWrapper/index.ts index 5ce8e4f..c97d89e 100644 --- a/src/modules/ImmolateWrapper/index.ts +++ b/src/modules/ImmolateWrapper/index.ts @@ -28,6 +28,7 @@ import { import type {Voucher} from "../balatrots/enum/Voucher.ts"; import {Edition, EditionItem} from "../balatrots/enum/Edition.ts"; import {Seal, SealItem} from "../balatrots/enum/Seal.ts"; +import {sanitizeSeed} from "../utils.ts"; export type SpoilableItems = "The Soul" | "Judgement" | "Wraith"; export interface MiscCardSource { @@ -480,7 +481,7 @@ export const getMiscCardSources: (maxCards: number) => Array = ( export function analyzeSeed(settings: AnalyzeSettings, analyzeOptions: AnalyzeOptions) { - const seed = settings.seed.toUpperCase().replace(/0/g, 'O').trim(); + const seed = sanitizeSeed(settings.seed); if (!seed) return; // Sanitize antes coming from settings (could be null/NaN/0 if UI or URL provided empty value) @@ -489,6 +490,7 @@ export function analyzeSeed(settings: AnalyzeSettings, analyzeOptions: AnalyzeOp const maxAntes = Math.max(1, safeAntes); const output = new SeedResultsContainer(); + // isLoading starts true in constructor, analysis populates antes, provider sets false after const deck = new Deck(deckMap[settings.deck]) const stake = new Stake(settings.stake as StakeType) const version = Number(settings.gameVersion) diff --git a/src/modules/state/analysisResultProvider.tsx b/src/modules/state/analysisResultProvider.tsx index 5a1cb39..6f8b779 100644 --- a/src/modules/state/analysisResultProvider.tsx +++ b/src/modules/state/analysisResultProvider.tsx @@ -1,11 +1,12 @@ -import React, {createContext, useContext, useMemo} from "react"; +import React, {createContext, useContext, useEffect, useState, useTransition} from "react"; import {analyzeSeed} from "../ImmolateWrapper"; import {useCardStore} from "./store.ts"; import {useSeedOptionsContainer} from "./optionsProvider.tsx"; -import type {SeedResultsContainer} from "../ImmolateWrapper/CardEngines/Cards.ts"; +import { SeedResultsContainer } from "../ImmolateWrapper/CardEngines/Cards.ts"; export const SeedResultContext = createContext(null); +export const SeedResultLoadingContext = createContext(false); export function useSeedResultsContainer() { const context = useContext(SeedResultContext); @@ -15,23 +16,43 @@ export function useSeedResultsContainer() { return context; } +export function useSeedResultsLoading() { + return useContext(SeedResultLoadingContext); +} + export function SeedResultProvider({children}: {children: React.ReactNode}) { const start = useCardStore(state => state.applicationState.start); - const analyzeState = useCardStore(state =>state.immolateState); + const analyzeState = useCardStore(state => state.immolateState); const options = useSeedOptionsContainer() - const seedResult = useMemo(()=> { - if (!start) { - return null; + const [seedResult, setSeedResult] = useState(null); + const [isPending, startTransition] = useTransition(); + + useEffect(() => { + if (!start || !analyzeState.seed) { + setSeedResult(null); + return; } - return analyzeSeed(analyzeState, options) - }, [analyzeState, options, start]); + + startTransition(() => { + try { + const result = analyzeSeed(analyzeState, options); + if (result) result.isLoading = false; + setSeedResult(result ?? null); + } catch (error) { + console.error('Failed to analyze seed:', error); + setSeedResult(null); + } + }); + }, [start, analyzeState, options]); return ( - {children} + + {children} + ) -} +} \ No newline at end of file diff --git a/src/modules/state/downloadProvider.tsx b/src/modules/state/downloadProvider.tsx index e33a62e..febe202 100644 --- a/src/modules/state/downloadProvider.tsx +++ b/src/modules/state/downloadProvider.tsx @@ -1,7 +1,8 @@ -import React, {createContext, useCallback, useContext} from "react"; +import React from "react"; +import { createContext, useCallback, useContext } from "react"; import {useCardStore} from "./store.ts"; import {useSeedOptionsContainer} from "./optionsProvider.tsx"; -import {useSeedResultsContainer} from "./analysisResultProvider.tsx"; +import {SeedResultContext} from "./analysisResultProvider.tsx"; export type DownloadSeedResultFunction = () => void; export const DownloadSeedResultContext = createContext(undefined); @@ -9,7 +10,7 @@ export const DownloadSeedResultContext = createContext state.immolateState); const options = useSeedOptionsContainer() - const SeedResults = useSeedResultsContainer() + const SeedResults = useContext(SeedResultContext); const downloadImmolateResults = useCallback(() => { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent( diff --git a/src/modules/state/store.ts b/src/modules/state/store.ts index 15f7197..55e36c7 100644 --- a/src/modules/state/store.ts +++ b/src/modules/state/store.ts @@ -2,6 +2,7 @@ import { create } from "zustand/index"; import { combine, createJSONStorage, devtools, persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; import { LOCATIONS, LOCATION_TYPES, options } from "../const.ts"; +import { sanitizeSeed } from "../utils.ts"; import type { StateStorage } from "zustand/middleware"; import type { BuyMetaData } from "../classes/BuyMetaData.ts"; import type { SeedResultsContainer } from "../ImmolateWrapper/CardEngines/Cards.ts"; @@ -167,7 +168,7 @@ const blueprintStorage: StateStorage = { getItem: (): string => { const immolateState = getImmolateStateFromUrl(); - + const hasSeed = !!immolateState.seed; const results = { state: { immolateState: { @@ -176,7 +177,8 @@ const blueprintStorage: StateStorage = { }, applicationState: { ...initialState.applicationState, - start: !!immolateState.seed + start: hasSeed, + settingsOpen: !hasSeed, }, shoppingState: { ...initialState.shoppingState, @@ -209,8 +211,10 @@ function getImmolateStateFromUrl() { const params = new URLSearchParams(window.location.search); const antesParam = params.get('antes'); const parsedAntes = antesParam !== null && antesParam !== '' && !Number.isNaN(Number(antesParam)) ? Number(antesParam) : undefined; + const seedParam = params.get('seed'); + const parsedSeed = seedParam ? sanitizeSeed(seedParam) : initialState.immolateState.seed; return { - seed: params.get('seed') || initialState.immolateState.seed, + seed: parsedSeed, deck: params.get('deck') || initialState.immolateState.deck, cardsPerAnte: parseInt(params.get('cardsPerAnte') || initialState.immolateState.cardsPerAnte.toString()), antes: parsedAntes !== undefined ? parsedAntes : initialState.immolateState.antes, @@ -232,10 +236,14 @@ export const useCardStore = create set((prev) => { - prev.immolateState.seed = seed.toUpperCase(); + const sanitized = sanitizeSeed(seed); + prev.immolateState.seed = sanitized; prev.shoppingState = initialState.shoppingState prev.searchState = initialState.searchState; prev.applicationState.hasSettingsChanged = true; + if (sanitized.length === 0) { + prev.applicationState.start = false; + } }, undefined, 'Global/SetSeed'), setDeck: (deck: string) => set((prev) => { prev.immolateState.deck = deck From 62e85a5ac5577bfcd8194af86b4a19370608cea3 Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 20:57:47 -0600 Subject: [PATCH 3/9] Update footer.tsx --- src/components/blueprint/layout/footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/blueprint/layout/footer.tsx b/src/components/blueprint/layout/footer.tsx index ff222a7..53aefea 100644 --- a/src/components/blueprint/layout/footer.tsx +++ b/src/components/blueprint/layout/footer.tsx @@ -17,7 +17,7 @@ import {GaEvent} from "../../../modules/useGA.ts"; export default function Footer() { - const {data: supporters, isPending} = useQuery>({ + const {data: supporters, isPending: isPending} = useQuery>({ queryKey: ['supporters'], queryFn: async () => { const response = await fetch('https://ttyyetpmvt.a.pinggy.link/supporters', { From e2241b618890c70a7e4c3f75d930518c9a8bcb55 Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 20:57:51 -0600 Subject: [PATCH 4/9] Update index.ts --- src/modules/ImmolateWrapper/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/ImmolateWrapper/index.ts b/src/modules/ImmolateWrapper/index.ts index d968296..ecd452c 100644 --- a/src/modules/ImmolateWrapper/index.ts +++ b/src/modules/ImmolateWrapper/index.ts @@ -31,6 +31,7 @@ import { Edition, EditionItem } from "../balatrots/enum/Edition.ts"; import { Seal, SealItem } from "../balatrots/enum/Seal.ts"; import type { DeckCard } from "../deckUtils.ts"; +import {sanitizeSeed} from "../utils.ts"; export type SpoilableItems = "The Soul" | "Judgement" | "Wraith"; export interface MiscCardSource { From 07df59b29d3a17f44e6456060601bbbecd982609 Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 21:16:01 -0600 Subject: [PATCH 5/9] remove the isloading and extra consolelog --- src/modules/state/analysisResultProvider.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/modules/state/analysisResultProvider.tsx b/src/modules/state/analysisResultProvider.tsx index 70ecd8b..6815a50 100644 --- a/src/modules/state/analysisResultProvider.tsx +++ b/src/modules/state/analysisResultProvider.tsx @@ -6,11 +6,9 @@ import type { SeedResultsContainer } from "../ImmolateWrapper/CardEngines/Cards. export const SeedResultContext = createContext(null); -export const SeedResultLoadingContext = createContext(false); export function useSeedResultsContainer() { const context = useContext(SeedResultContext); - console.log(context) if (context === null) { throw new Error("useSeedResultsContainer must be used within a SeedResultProvider"); } @@ -30,16 +28,13 @@ export function SeedResultProvider({ children }: { children: React.ReactNode }) return analyzeSeed(analyzeState, { ...options, customDeck: deckState.cards - }) + }); }, [analyzeState, deckState.cards, options, start]); + return ( - {children} - ) - - } \ No newline at end of file From 61ccc58f3b3793c26b2d9d06f762e11c10c86a38 Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 21:17:15 -0600 Subject: [PATCH 6/9] use the sanitize seed properly to fix bug --- src/modules/state/store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/state/store.ts b/src/modules/state/store.ts index 6fa4450..5ac5155 100644 --- a/src/modules/state/store.ts +++ b/src/modules/state/store.ts @@ -271,7 +271,8 @@ export const useCardStore = create()( prev.applicationState.viewMode = viewMode; }, undefined, 'Global/SetViewMode'), setSeed: (seed) => set((prev) => { - prev.immolateState.seed = seed.toUpperCase(); + const sanitized = sanitizeSeed(seed); + prev.immolateState.seed = sanitized; prev.shoppingState = initialState.shoppingState prev.searchState = initialState.searchState; prev.applicationState.hasSettingsChanged = true; From 30ea5fdd07afe7d3ca1d1c4ffde1af4f7824a104 Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 21:18:23 -0600 Subject: [PATCH 7/9] use dirty state from external changes to keep in sync :) --- src/components/SeedInputAutoComplete.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/SeedInputAutoComplete.tsx b/src/components/SeedInputAutoComplete.tsx index 52d8c11..0b3609f 100644 --- a/src/components/SeedInputAutoComplete.tsx +++ b/src/components/SeedInputAutoComplete.tsx @@ -28,11 +28,18 @@ interface SeedInputProps { function SeedInputAutoComplete({ seed, setSeed, w, showDeckSelect, label = 'Seed', placeholder = 'Enter Seed' }: SeedInputProps) { const [localSeed, setLocalSeed] = useState(seed); + const isDirty = useRef(false); + + // Sync from store when not actively editing + if (!isDirty.current && localSeed !== seed) { + setLocalSeed(seed); + } const debouncedSetSeed = useDebouncedCallback((value: string) => { setLocalSeed(sanitizeSeed(value)); if (value) setSeed(value); - }, 200); + isDirty.current = false; + }, 160); const deck = useCardStore(state => state.immolateState.deck); const setDeck = useCardStore(state => state.setDeck); @@ -81,9 +88,11 @@ function SeedInputAutoComplete({ seed, setSeed, w, showDeckSelect, label = 'Seed data={seedAutoCompleteData} value={localSeed} onChange={(value) => { + isDirty.current = true; setLocalSeed(value); if (allSuggestions.includes(value)) { setSeed(value); + isDirty.current = false; } else { debouncedSetSeed(value); } From ad95b986176950b224faeb63158017e0cfc6291b Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 21:30:34 -0600 Subject: [PATCH 8/9] Update canvasRenderer.tsx --- src/components/Rendering/canvasRenderer.tsx | 101 ++++++++++---------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/src/components/Rendering/canvasRenderer.tsx b/src/components/Rendering/canvasRenderer.tsx index eea3ee6..0a09ad0 100644 --- a/src/components/Rendering/canvasRenderer.tsx +++ b/src/components/Rendering/canvasRenderer.tsx @@ -2,7 +2,6 @@ import {Layer} from "../../modules/classes/Layer.ts"; import {useEffect, useRef, useState} from "react"; import {useForceUpdate, useHover, useMergedRef, useMouse, useResizeObserver} from "@mantine/hooks"; import {AspectRatio} from "@mantine/core"; -import React from "react"; const globalImageCache = new Map(); interface RenderCanvasProps { @@ -110,63 +109,59 @@ interface SimpleRenderProps { -export const SimpleRenderCanvas = React.forwardRef( - ({ layers, invert = false }, ref) => { - const canvasRef = useRef(null); - const [ratio, setRatio] = useState(3 / 4); - const forceUpdate = useForceUpdate(); - useEffect(() => { - const actualRef = (ref && 'current' in ref) ? ref : canvasRef; - if (!actualRef.current || !layers || layers.length === 0) return; - const canvas = actualRef.current; - const context = canvas.getContext('2d'); - if (!context) return; +export function SimpleRenderCanvas({ layers, invert = false }: SimpleRenderProps) { + const canvasRef = useRef(null); + const [ratio, setRatio] = useState(3 / 4); + const forceUpdate = useForceUpdate(); + useEffect(() => { + if (!canvasRef.current || !layers || layers.length === 0) return; + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + if (!context) return; context.clearRect(0, 0, canvas.width, canvas.height); - context.clearRect(0, 0, canvas.width, canvas.height); - - layers - .sort((a, b) => a.order - b.order) - .forEach(layer => { - if (globalImageCache.has(layer.source)) { - const image = globalImageCache.get(layer.source) as HTMLImageElement; - const imageRatio = renderImage(canvas, context, image, layer); + + layers + .sort((a, b) => a.order - b.order) + .forEach(layer => { + if (globalImageCache.has(layer.source)) { + const image = globalImageCache.get(layer.source) as HTMLImageElement; + const imageRatio = renderImage(canvas, context, image, layer); + if (layer.order === 0) { + setRatio(imageRatio); + } + return; + } + loadImage(layer.source) + .then((img: HTMLImageElement) => { + const imageRatio = renderImage(canvas, context, img, layer); + globalImageCache.set(layer.source, img); if (layer.order === 0) { setRatio(imageRatio); } - return; - } - loadImage(layer.source) - .then((img: HTMLImageElement) => { - const imageRatio = renderImage(canvas, context, img, layer); - globalImageCache.set(layer.source, img); - if (layer.order === 0) { - setRatio(imageRatio); - } - forceUpdate() - }) - }); - - if (invert) { - canvas.style.filter = 'invert(0.8)'; - } else { - canvas.style.filter = 'none'; - } - }, [layers, invert]); - - return ( - - - - ); - } -); + forceUpdate() + }) + }); + + if (invert) { + canvas.style.filter = 'invert(0.8)'; + } else { + canvas.style.filter = 'none'; + } + }, [layers, invert]); + + return ( + + + + ); +} From b4a2ffc6241af8042d3c6f3623209cd264829eae Mon Sep 17 00:00:00 2001 From: "Nathanial P. Howard" Date: Sat, 7 Feb 2026 21:32:32 -0600 Subject: [PATCH 9/9] Update SeedInputAutoComplete.tsx --- src/components/SeedInputAutoComplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SeedInputAutoComplete.tsx b/src/components/SeedInputAutoComplete.tsx index 0b3609f..879a620 100644 --- a/src/components/SeedInputAutoComplete.tsx +++ b/src/components/SeedInputAutoComplete.tsx @@ -119,7 +119,7 @@ export function QuickAnalyze() { label="Analyze Seed" />