Uh oh!
There was an error while loading. Please reload this page.
feat(codes): share scannable-code geometry via KMP; export tip codes as PNG/SVG - #1287
Merged
Merged
Conversation
…as PNG/SVG Tip cards need PNG and SVG export. Rather than write that twice, the part that actually has to agree between platforms -- where every mark goes -- moves into a new KMP module, and each platform keeps only a thin painter over it. :libs:codes:kikcode (commonMain, no platform APIs): - KikCodeGeometry.describe(payload, dimension) -> KikCodeDescription: the centre, badge well, dot diameter, and an ordered list of Dot/Arc/Ring marks. Runs of consecutive set bits collapse to one stroked arc with round caps -- the same union as per-bit dots plus connecting bands, far fewer elements. - KikCodeSvg.render(...) -> a self-contained SVG document. Export is code-only (matching the existing share preview), so there is no text and no font to embed. - Exported through :kmp:shared-core, so iOS gets it in the SharedCore XCFramework. Android is now a painter, not a second implementation. KikCodeContentRendererImpl drops from 139 lines of fused maths-and-drawing to ~30 lines of draw calls, so the on-screen view, the exported PNG, the exported SVG, and iOS are all the same numbers. TipCodeExporter writes either format to cacheDir/share_exports/ behind the existing FileProvider. Framing: adopt iOS's canonical outer ratio of 0.5. Android used to inset the graphic to 0.93 and then have KikCodeContentView overscan by 1.03 to compensate, a net 0.958; the code is now ~4.2% larger within the same box and the badge well grows from 0.298 to 0.32 of the graphic, matching iOS. Gated by test-vectors/kikcode.json plus a golden SVG, generated by an independent Python implementation written from the spec ratios rather than transcribed from the Kotlin. Both run on the JVM host and on Kotlin/Native: the byte-for-byte SVG comparison is what proves the two toolchains agree, since Double.toString() is unspecified across Kotlin targets and libm trig can differ in the last place (both handled by formatting every emitted number at 3 decimals via roundToLong). Fixtures are compiled into commonTest as generated Kotlin -- Kotlin/Native test binaries ship no resource bundle, so a resource-based loader silently only ever runs on the JVM. Five Robolectric tests cover what the vectors cannot reach: that arc sweeps are not inverted, that the badge fills its well, and that the graphic frames correctly. TipCodeExporter has no UI entry point yet; that lands with the You tab work.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 20, 2026
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 freeto 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.
Tip cards need PNG and SVG export. Rather than write that twice, the part that actually has to agree between platforms — where every mark goes — moves into a KMP module, and each platform keeps only a thin painter over it.
Rasterisation stays native deliberately — Skiko/CMP would add several MB to the iOS framework to produce byte-identical PNGs nobody needs. Only the maths is shared, and the maths is the part that can drift.
:libs:codes:kikcodePure Kotlin, no platform APIs:
KikCodeGeometry.describe(payload, dimension)→KikCodeDescription: the centre, badge well, dot diameter, and an ordered list ofDot/Arc/Ringmarks. Runs of consecutive set bits collapse into a single stroked arc with round caps — geometrically the same union as per-bit dots plus connecting bands, with far fewer elements, which is what keeps the SVG small.KikCodeSvg.render(...)→ a self-contained SVG document. Export is code-only (matching what the share preview already shows), so there is no text and therefore no font to embed.Exported through
:kmp:shared-core, so iOS picks it up in theSharedCoreXCFramework.Android becomes a painter, not a second implementation
KikCodeContentRendererImpldrops from 139 lines of fused maths-and-drawing to ~30 lines of draw calls. The on-screen view, the exported PNG, the exported SVG, and iOS now come off the same numbers.TipCodeExporterwrites either format tocacheDir/share_exports/behind the existingFileProvider.The parity gate
test-vectors/kikcode.jsonplus a golden SVG, produced by an independent Python implementation written from the spec ratios rather than transcribed from the Kotlin — so a match is corroboration, not a tautology. Both run on the JVM host and on Kotlin/Native.The byte-for-byte SVG comparison is the one that earns its keep:
Double.toString()is unspecified across Kotlin targets and libmcos/sincan differ in the last place, so every emitted number is fixed to 3 decimals viaroundToLong()(ties toward positive infinity — the only tie-break Kotlin actually specifies).Getting the native half to run at all needed a fix: Kotlin/Native test binaries ship no resource bundle, so
NSBundle.pathForResourcefinds nothing and a resource-based loader silently only ever runs on the JVM. Fixtures are now compiled intocommonTestas generated Kotlin, which works on every target and drops theexpect/actualloader.Five Robolectric tests cover what the vectors cannot reach — that arc sweeps aren't inverted, that the badge fills its well, and that the graphic frames correctly.
Worth a look on device
The on-screen code is ~4.2% larger. Android used to inset the graphic to
0.93and then haveKikCodeContentViewoverscan by1.03to compensate (a net0.958); iOS just uses0.5. This adopts iOS's canonical value so the export and the on-screen code can't disagree — but it's a visible change to the bill and tip card, and the badge well grows from0.298to0.32of the graphic to match iOS.Follow-ups
TipCodeExporterhas no UI entry point yet — that lands with the new You tab design work.KikCode.generateDescriptionfor the exported geometry, add aCGContextpainter) is acode-ios-appchange.:libs:encryption:base58'siosTesthas the same missingExperimentalForeignApiopt-in, which means its Kotlin/Native vector test has never compiled. Its iOS parity runs throughFlipcashCoreVectorsinstead, so coverage isn't actually missing, but the dead source is misleading.