From 880831b0b715d0fe7a4bd2bc29df02813884c257 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sat, 25 Jul 2026 20:04:46 +0900 Subject: [PATCH 1/2] fix: keep getColorFromString finite for long strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hash accumulator grows without bound: acc * 31 + code leaves exact integer range at 11 characters and overflows to Infinity past ~200, at which point Infinity % 360 is NaN and the function returns an unparseable hsl(NaN, 100%, 50%, 1). Net ids are selector paths like 'group > capacitor.C101 > port.pin1 to U100.VOUT', so nested groups reach that length in practice — about 22 levels of nesting is enough. Keep the accumulator in signed 32-bit range, which stays finite and exact for any input. Hues change for inputs longer than 10 characters, so snapshots are regenerated. --- lib/utils/getColorFromString.ts | 23 +- .../bug-report-20260706T213649Z.snap.svg | 354 ++++++------ .../bug-report-20260706T220324Z.snap.svg | 310 +++++----- .../bug-report-20260707T020342Z.snap.svg | 14 +- .../bug-report-20260707T092615Z.snap.svg | 350 ++++++------ .../bug-report-20260707T134549Z.snap.svg | 120 ++-- .../bug-report-20260707T134722Z.snap.svg | 44 +- .../bug-report-20260707T140410Z.snap.svg | 142 ++--- .../bug-report-20260707T141025Z.snap.svg | 30 +- .../bug-report-20260707T141421Z.snap.svg | 80 +-- .../bug-report-20260707T230831Z.snap.svg | 32 +- .../bug-report-20260708T053736Z.snap.svg | 2 +- .../bug-report-20260708T055430Z.snap.svg | 84 +-- .../bug-report-20260708T095725Z.snap.svg | 50 +- .../bug-report-20260716T144856Z.snap.svg | 96 ++-- .../bug-report-20260717T022934Z.snap.svg | 78 +-- .../bug-report-20260717T031704Z.snap.svg | 4 +- .../bug-report-20260717T042845Z.snap.svg | 6 +- .../bug-report-20260721T221026Z.snap.svg | 536 +++++++++--------- .../bug-report-20260724T175257Z.snap.svg | 14 +- .../examples/__snapshots__/example01.snap.svg | 8 +- .../examples/__snapshots__/example02.snap.svg | 10 +- .../examples/__snapshots__/example03.snap.svg | 86 +-- .../examples/__snapshots__/example04.snap.svg | 4 +- .../examples/__snapshots__/example05.snap.svg | 10 +- .../examples/__snapshots__/example06.snap.svg | 6 +- .../examples/__snapshots__/example07.snap.svg | 14 +- .../examples/__snapshots__/example08.snap.svg | 10 +- .../examples/__snapshots__/example09.snap.svg | 74 +-- .../examples/__snapshots__/example10.snap.svg | 30 +- .../examples/__snapshots__/example11.snap.svg | 18 +- .../examples/__snapshots__/example12.snap.svg | 12 +- .../examples/__snapshots__/example13.snap.svg | 38 +- .../examples/__snapshots__/example14.snap.svg | 26 +- .../examples/__snapshots__/example15.snap.svg | 24 +- .../examples/__snapshots__/example16.snap.svg | 12 +- .../examples/__snapshots__/example17.snap.svg | 18 +- .../examples/__snapshots__/example18.snap.svg | 16 +- .../examples/__snapshots__/example19.snap.svg | 14 +- .../examples/__snapshots__/example20.snap.svg | 8 +- .../examples/__snapshots__/example21.snap.svg | 38 +- .../examples/__snapshots__/example22.snap.svg | 12 +- .../examples/__snapshots__/example23.snap.svg | 18 +- .../examples/__snapshots__/example24.snap.svg | 18 +- .../examples/__snapshots__/example25.snap.svg | 32 +- .../examples/__snapshots__/example26.snap.svg | 18 +- .../examples/__snapshots__/example27.snap.svg | 20 +- .../examples/__snapshots__/example28.snap.svg | 12 +- .../examples/__snapshots__/example29.snap.svg | 174 +++--- .../examples/__snapshots__/example30.snap.svg | 46 +- .../examples/__snapshots__/example31.snap.svg | 4 +- .../examples/__snapshots__/example32.snap.svg | 58 +- .../examples/__snapshots__/example33.snap.svg | 34 +- .../examples/__snapshots__/example34.snap.svg | 14 +- .../examples/__snapshots__/example35.snap.svg | 20 +- .../examples/__snapshots__/example36.snap.svg | 12 +- .../examples/__snapshots__/example37.snap.svg | 30 +- .../examples/__snapshots__/example38.snap.svg | 4 +- .../examples/__snapshots__/example39.snap.svg | 10 +- .../examples/__snapshots__/example40.snap.svg | 46 +- .../examples/__snapshots__/example41.snap.svg | 14 +- .../examples/__snapshots__/example42.snap.svg | 22 +- .../examples/__snapshots__/example43.snap.svg | 26 +- .../examples/__snapshots__/example44.snap.svg | 50 +- .../examples/__snapshots__/example45.snap.svg | 26 +- .../examples/__snapshots__/example46.snap.svg | 28 +- .../examples/__snapshots__/example47.snap.svg | 32 +- .../examples/__snapshots__/example48.snap.svg | 16 +- .../examples/__snapshots__/example49.snap.svg | 86 +-- .../examples/__snapshots__/example50.snap.svg | 34 +- .../bugreport-001-gnd-overlap.snap.svg | 22 +- .../manufacturePartNumber-text-box.snap.svg | 2 +- ...abel-connector-through-rail-label.snap.svg | 24 +- .../repro-atmega328p-fault-pullup.snap.svg | 82 +-- ...o-atmega328p-missing-gnd-netlabel.snap.svg | 140 ++--- .../repro-bq24074-battery-charger.snap.svg | 54 +- .../__snapshots__/repro-cc2340r5.snap.svg | 28 +- ...ro-core-subcircuit-missing-ground.snap.svg | 26 +- ...example35-minimize-trace-crossing.snap.svg | 188 +++--- .../repro-ina237-current-monitor.snap.svg | 70 +-- .../repro-missing-trace-netlabel.snap.svg | 14 +- .../repro-netlabel-collision-687.snap.svg | 42 +- .../repro-netlabel-overlap-trace.snap.svg | 12 +- .../repro-rectifier-trace-overlap.snap.svg | 14 +- ...ro-rp2040-gamepad-trace-alignment.snap.svg | 162 +++--- ...0-zero-crystal-fallback-netlabels.snap.svg | 6 +- ...repro-tps61222-trace-intersection.snap.svg | 12 +- ...ro129-host-custom-symbol-passives.snap.svg | 26 +- ...27441-fuel-gauge-trace-through-c1.snap.svg | 42 +- .../repro47-endpoint-obstacle-detour.snap.svg | 18 +- ...pro5-escape-padded-text-obstacles.snap.svg | 10 +- ...repro51-overlap-junction-crossing.snap.svg | 16 +- ...variant-resistor-facing-direction.snap.svg | 80 +-- .../trace-overlap-box-resistor.snap.svg | 8 +- ...ineSolver2_01-example17-d1_1-u1_1.snap.svg | 2 +- .../__snapshots__/TraceCleanupSolver.snap.svg | 2 +- .../TraceLabelOverlapAvoidanceSolver.snap.svg | 2 +- .../renderComparisonView01.snap.svg | 18 +- .../renderComparisonView02.snap.svg | 2 +- .../renderComparisonView03.snap.svg | 2 +- .../MergedNetLabelObstacles.snap.svg | 2 +- .../OverlapAvoidanceStepSolver.snap.svg | 2 +- .../SingleOverlapSolver.snap.svg | 2 +- 103 files changed, 2454 insertions(+), 2439 deletions(-) diff --git a/lib/utils/getColorFromString.ts b/lib/utils/getColorFromString.ts index 6332e2d44..3498b946f 100644 --- a/lib/utils/getColorFromString.ts +++ b/lib/utils/getColorFromString.ts @@ -1,7 +1,22 @@ export const getColorFromString = (string: string, alpha = 1) => { // pseudo random number from string - const hash = string.split("").reduce((acc, char) => { - return acc * 31 + char.charCodeAt(0) - }, 0) - return `hsl(${hash % 360}, 100%, 50%, ${alpha})` + // + // `acc * 31 + code` grows without bound: it leaves exact-integer range at + // 11 characters and overflows to Infinity past ~200, at which point + // `Infinity % 360` is NaN and the result is an unparseable `hsl(NaN, ...)`. + // Net ids here are selector paths such as + // `group > capacitor.C101 > port.pin1 to U100.VOUT`, so deeply nested + // groups reach that length in practice. + // + // Keeping the accumulator in signed 32-bit range via `| 0` is the standard + // string-hash formulation and stays finite and exact for any input. + let hash = 0 + for (let i = 0; i < string.length; i++) { + hash = (hash * 31 + string.charCodeAt(i)) | 0 + } + + // `%` keeps the sign of the dividend, so normalise into [0, 360). + const hue = ((hash % 360) + 360) % 360 + + return `hsl(${hue}, 100%, 50%, ${alpha})` } diff --git a/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg b/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg index b98797851..285e0c069 100644 --- a/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +++ b/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg @@ -1,4 +1,4 @@ - + ]]> \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg b/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg index 142036040..2a7e34018 100644 --- a/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +++ b/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg @@ -1,22 +1,22 @@ - + ]]> \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg b/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg index 22f89c935..e062b340e 100644 --- a/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg +++ b/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg @@ -1,5 +1,5 @@ - + ]]> \ No newline at end of file diff --git a/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_repro47-short-opposing-pins.snap.svg b/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_repro47-short-opposing-pins.snap.svg index 3fa8f8d2e..23d46b87a 100644 --- a/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_repro47-short-opposing-pins.snap.svg +++ b/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_repro47-short-opposing-pins.snap.svg @@ -1,6 +1,6 @@ - + ]]> \ No newline at end of file