From f4d313464ed0124e19a1d84441edf2c8eba2bdc8 Mon Sep 17 00:00:00 2001 From: Fini Date: Tue, 24 Feb 2026 04:06:19 +0800 Subject: [PATCH] fix(ai): normalize gradient stop offsets to prevent NaN display AI sometimes omits the offset field on gradient stops, causing Math.round(undefined * 100) = NaN in the UI. Fix at two layers: - normalize-pen-file.ts: auto-distribute missing offsets evenly across [0,1] and convert percentage-format offsets (>1) to 0-1 range - fill-section.tsx: defensive guard in the stop percentage display --- src/components/panels/fill-section.tsx | 2 +- src/utils/normalize-pen-file.ts | 28 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/components/panels/fill-section.tsx b/src/components/panels/fill-section.tsx index cc173b7c7..82fc24558 100644 --- a/src/components/panels/fill-section.tsx +++ b/src/components/panels/fill-section.tsx @@ -221,7 +221,7 @@ export default function FillSection({ onChange={(c) => handleStopColorChange(i, c)} /> handleStopOffsetChange(i, v)} min={0} max={100} diff --git a/src/utils/normalize-pen-file.ts b/src/utils/normalize-pen-file.ts index abe0a9097..181398f6e 100644 --- a/src/utils/normalize-pen-file.ts +++ b/src/utils/normalize-pen-file.ts @@ -156,19 +156,31 @@ function normalizeSingleFill( function normalizeGradientStops( raw: unknown[] | undefined, ): GradientStop[] { - if (!Array.isArray(raw)) return [] - return raw.map((s: unknown) => { + if (!Array.isArray(raw) || raw.length === 0) return [] + + // First pass: parse offsets, collecting which ones are explicitly set + const parsed = raw.map((s: unknown) => { const stop = s as Record + const rawOffset = + typeof stop.offset === 'number' && Number.isFinite(stop.offset) + ? stop.offset + : typeof stop.position === 'number' && Number.isFinite(stop.position) + ? stop.position + : null + // Normalize percentage-format offsets (AI sometimes outputs 0-100 instead of 0-1) + const offset = rawOffset !== null && rawOffset > 1 ? rawOffset / 100 : rawOffset return { - offset: - typeof stop.offset === 'number' && Number.isFinite(stop.offset) - ? stop.offset - : typeof stop.position === 'number' && Number.isFinite(stop.position) - ? stop.position - : 0, + offset, color: typeof stop.color === 'string' ? stop.color : '#000000', } }) + + // Second pass: auto-distribute any stops that are missing an offset + const n = parsed.length + return parsed.map((s, i) => ({ + color: s.color, + offset: s.offset !== null ? Math.max(0, Math.min(1, s.offset!)) : i / Math.max(n - 1, 1), + })) } // ---------------------------------------------------------------------------