Repository navigation
refactor(core)!: replace the hybrid contrast solver with chroma-column tracing - #228
Merged
Merged
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This was referenced Oct 7, 2026
Owner
Author
This was referenced Oct 8, 2026
Owner
Author
pbroom
changed the base branch from
perf/core-contrast-imports
to
graphite-base/228
October 10, 2026 19:43
…n tracing
Contrast regions now have one solver, built from the chroma-column
prototype ("option C"). Pass/fail depends only on a sample's displayed
luminance, so each side of a region is one luminance level curve. The
solver traces it in columns of fixed chroma-to-lightness ratio (rays from
black), which cover the GAMUT_EPSILON slack near black where l < c: along a
ray every linear channel is r^3 times a constant, the displayed luminance
never decreases inside the gamut, and the ray's gamut edge is explicit.
- Luminance is measured with the target-gamut clipping contrastRatio /
contrastAPCA use, the gamut edge uses the shared linear bounds, and the
near-black strict bound is gone. Side targets come from a bisection with
the public metric's own arithmetic, kept a hair inside the passing side,
and piece ends are verified with the public checks, so every returned
point passes the check and is in gamut per inSrgbGamut / inP3Gamut.
- Gamut events (where a piece starts or ends) are found on a fixed
internal scan of the edge-visibility function, independent of the
sampling options: 256 uniform angles plus every edge breakpoint (two
channel bounds meeting, a channel constant changing sign, clipping
starting, the maxChroma cap), all exact cubic roots, plus a search of
every local extremum near zero. This replaces the prototype's probeFin /
locateEdge, which missed fins and mixed up crossings in one bracket.
- Next to every edge corner the scan also tests one-sided slopes for a
turn toward zero, so a gamut notch narrower than a scan step (the edge
switching from blue > 1 to red < 0 and back near h 264.206) splits the
contour instead of being bridged.
- Chords are checked exactly against the gamut (each channel is a cubic
along a chord, tested at its turning points) and split while they leave
it; sampling is clamped and spread over the hue's gamut chroma extent
(and the angle a piece sweeps), not [0, maxChroma].
- The reference is measured unmapped, as the public checks clip it. A
threshold equal to the contrast against white returns white as a
one-point path, and traces count dropped pieces (droppedPieceCount).
New sampling options: initialSamples (default 32), errorTolerance (default
0.0015), maxDepth (default 6), validated like the old steps and clamped
(512, 1e-6, 12). Traces record solver 'contrast-rays' with fidelity info.
ContrastRegionLayer maps quality and drag freeze onto them, with
interactive defaults 8 / 0.004 / 3.
BREAKING CHANGE: the hybrid solver is removed. lightnessSteps, chromaSteps,
hybridMaxDepth, hybridErrorTolerance, tolerance, and maxIterations are type
errors and throw a TypeError (in contrastRegionPaths, plane queries, and
ContrastRegionLayer props). degradedReason, ContrastHybridDegradedReason,
the hybrid-only trace stages and their types, and the cusp / rootFinding /
branching timing keys are removed. ContrastRegionLayerMetrics reports
initialSamples, errorTolerance, and maxDepth.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
simplifyPolyline ran after the exact chord gamut checks and could replace checked segments with unchecked ones. Simplification now runs inside the solver and only collapses a span when the replacing chord passes the same exact cubic gamut check (allowing only the rounding its own end points carry). Adds simplifyTolerance coverage on the fold-band, fin, and notch cases. Co-Authored-By: Claude Opus 5.5 <[email protected]>
ContrastRegionLayer requested and reported raw sampling props, while the solver clamps them (initialSamples to 512, maxDepth to 12, errorTolerance to at least 1e-6). Core now exports clampContrastRegionSampling, which resolveContrastSampling also uses, and the layer resolves its sampling once with it for both the request and the metrics. Co-Authored-By: Claude Opus 5.5 <[email protected]>
getPlaneContrastRegion and getPlaneContrastBoundary returned empty geometry on non-L x C planes before the options reached the solver, so invalid sampling, criterion, or removed options silently succeeded there. They now validate the options exactly as contrastRegionPaths does before the early return. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The docs said an out-of-gamut reference is gamut-mapped; the solver and the public metrics clip its channels as displayed. Fix the wording in the contrast API page and the gamut option's JSDoc, document clampContrastRegionSampling and gamut-safe simplification, and update the changeset. Co-Authored-By: Claude Opus 5.5 <[email protected]>
pbroom
force-pushed
the
refactor/core-chroma-contrast-solver
branch
from
October 10, 2026 19:45
319683f to
fa57305
Compare
pbroom
added a commit
that referenced
this pull request
Oct 10, 2026
IB-006 still described the legacy marching-squares and hybrid contrast engines as current. Mark them as replaced with PR references (#223 retired the legacy engine, #228 replaced the hybrid solver with the ray-based solver in contrast/region-solver.ts), record the new options and the removed ones, and list the two open follow-ups: 7 tiny P3 APCA pieces missed by the single-extremum scan, and ContrastRegionLayer still gamut-mapping its reference. The repo-wide sweep for region-legacy, region-hybrid, hybridMaxDepth, samplingMode and "legacy engine" found no other docs describing the removed engines as current; remaining hits are removal notices, changelog entries or the still-supported gamut/chroma-band samplingMode. Co-Authored-By: Claude Opus 5.5 <[email protected]>
pbroom
added a commit
that referenced
this pull request
Oct 10, 2026
…nt (#232) IB-006 still described the legacy marching-squares and hybrid contrast engines as current. Mark them as replaced with PR references (#223 retired the legacy engine, #228 replaced the hybrid solver with the ray-based solver in contrast/region-solver.ts), record the new options and the removed ones, and list the two open follow-ups: 7 tiny P3 APCA pieces missed by the single-extremum scan, and ContrastRegionLayer still gamut-mapping its reference. The repo-wide sweep for region-legacy, region-hybrid, hybridMaxDepth, samplingMode and "legacy engine" found no other docs describing the removed engines as current; remaining hits are removal notices, changelog entries or the still-supported gamut/chroma-band samplingMode. lint, build, test, format:check, typecheck, size, check:artifacts, check:preprod and agents:check:strict all pass on this branch. Part of the color-kit assessment stack (#204 → #232). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Contrast regions now have one solver, built from the chroma-column
prototype ("option C"). Pass/fail depends only on a sample's displayed
luminance, so each side of a region is one luminance level curve. The
solver traces it in columns of fixed chroma-to-lightness ratio (rays from
black), which cover the GAMUT_EPSILON slack near black where l < c: along a
ray every linear channel is r^3 times a constant, the displayed luminance
never decreases inside the gamut, and the ray's gamut edge is explicit.
contrastAPCA use, the gamut edge uses the shared linear bounds, and the
near-black strict bound is gone. Side targets come from a bisection with
the public metric's own arithmetic, kept a hair inside the passing side,
and piece ends are verified with the public checks, so every returned
point passes the check and is in gamut per inSrgbGamut / inP3Gamut.
internal scan of the edge-visibility function, independent of the
sampling options: 256 uniform angles plus every edge breakpoint (two
channel bounds meeting, a channel constant changing sign, clipping
starting, the maxChroma cap), all exact cubic roots, plus a search of
every local extremum near zero. This replaces the prototype's probeFin /
locateEdge, which missed fins and mixed up crossings in one bracket.
turn toward zero, so a gamut notch narrower than a scan step (the edge
switching from blue > 1 to red < 0 and back near h 264.206) splits the
contour instead of being bridged.
along a chord, tested at its turning points) and split while they leave
it; sampling is clamped and spread over the hue's gamut chroma extent
(and the angle a piece sweeps), not [0, maxChroma].
threshold equal to the contrast against white returns white as a
one-point path, and traces count dropped pieces (droppedPieceCount).
New sampling options: initialSamples (default 32), errorTolerance (default
0.0015), maxDepth (default 6), validated like the old steps and clamped
(512, 1e-6, 12). Traces record solver 'contrast-rays' with fidelity info.
ContrastRegionLayer maps quality and drag freeze onto them, with
interactive defaults 8 / 0.004 / 3.
BREAKING CHANGE: the hybrid solver is removed. lightnessSteps, chromaSteps,
hybridMaxDepth, hybridErrorTolerance, tolerance, and maxIterations are type
errors and throw a TypeError (in contrastRegionPaths, plane queries, and
ContrastRegionLayer props). degradedReason, ContrastHybridDegradedReason,
the hybrid-only trace stages and their types, and the cusp / rootFinding /
branching timing keys are removed. ContrastRegionLayerMetrics reports
initialSamples, errorTolerance, and maxDepth.
Verification
An independent reviewer, using its own brute-force harness built on the public contrast and gamut checks, ran three rounds. Its final verdict was safe to submit.
contrastRatio/contrastAPCAor the gamut check.Known limitation (follow-up): the solver misses 7 tiny P3 APCA pieces against black, 6e-6 to 6e-4 long, with gaps up to 0.0022. That happens where two edge-luminance minima fall inside one internal scan step (
region-solver.tssearches for a single extremum per scan interval).Breaking:
lightnessSteps/chromaStepsare replaced byinitialSamples,hybridErrorTolerancebyerrorTolerance, andhybridMaxDepthbymaxDepth.tolerance,maxIterationsanddegradedReasonare removed, and removed options throwTypeError.Part of the color-kit assessment stack (#204 → #228).
🤖 Generated with Claude Code