mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:27:02 +01:00
billsplit was a standalone Flutter app in ~/Downloads with one commit and no git remote — that directory was the only copy of its history. Three things the move needed beyond copying the tree: - The root .gitignore silently swallowed 32 of its files (*.png, *.ico, *.manifest). Ignored files do not error on `git add`, they just vanish, so this would have committed an app that cannot build. Added a trailing block of ! overrides plus .binary-allowlist (which did not exist yet) — the documented in-repo exception for binaries a build genuinely needs. The ../testsAndMisc_binaries/ symlink scheme does not apply: those directories are gone, and a symlinked PNG breaks a fresh clone. - billsplit/.github/workflows/ci.yaml was inert once nested; GitHub only reads workflows at the repository root. Relocated with a working-directory and a billsplit/** path filter. - The app's coverage gate moved to python_pkg/billsplit_coverage/ so every Python file stays under python_pkg/, and gained a test suite at 100% branch coverage as that location requires. billsplit/ now holds only Dart. Also replaces the stock Flutter icon with a family icon from the shared generator. The glyph is a filled silhouette because the family's 72px stroke cannot carry a fine torn edge, and the split is an evenodd-cut perforation so the holes stay transparent on the foreground and monochrome layers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
220 lines
9.6 KiB
Python
220 lines
9.6 KiB
Python
"""Per-app glyph artwork for the shared icon family.
|
|
|
|
A glyph is a raw SVG fragment drawn in the 1024x1024 master canvas. It does not
|
|
need to be perfectly positioned: :mod:`python_pkg.app_icons.render` measures the
|
|
rendered ink and recentres the fragment, so every icon in the family sits at the
|
|
same optical height.
|
|
|
|
Fragments inherit ``fill="none"``, ``stroke``, ``stroke-width`` and round caps
|
|
from the wrapping group. A fragment that needs a filled silhouette instead
|
|
overrides those locally and uses :data:`~python_pkg.app_icons.style.ACCENT_MARKER`
|
|
for its colour.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Glyph:
|
|
"""One app's artwork.
|
|
|
|
Parameters:
|
|
name (str): Stable identifier, used as the ``--glyph`` CLI value.
|
|
description (str): What the glyph depicts, for ``--list`` output.
|
|
body (str): SVG fragment drawn in the 1024x1024 master canvas.
|
|
"""
|
|
|
|
name: str
|
|
description: str
|
|
body: str
|
|
|
|
|
|
# A cloud silhouette outline with a download arrow underneath: a client that
|
|
# pulls files off a remote dufs server.
|
|
_CLOUD_DOWN = """\
|
|
<g transform="translate(296,196) scale(18)" stroke-width="4">
|
|
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 \
|
|
2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 \
|
|
0-2.64-2.05-4.78-4.65-4.96z"/>
|
|
</g>
|
|
<path d="M 512 628 L 512 744"/>
|
|
<path d="M 444 676 L 512 744 L 580 676"/>"""
|
|
|
|
# Bar, two plates and two collars. Plate spacing is well above the minimum
|
|
# negative space so the bar does not fill in at launcher size.
|
|
_BARBELL = """\
|
|
<path d="M 268 512 L 756 512"/>
|
|
<path d="M 364 356 L 364 668"/>
|
|
<path d="M 660 356 L 660 668"/>
|
|
<path d="M 268 432 L 268 592"/>
|
|
<path d="M 756 432 L 756 592"/>"""
|
|
|
|
# Plain clock face. Deliberately no bell "ears": at launcher size they read as
|
|
# noise rather than as an alarm clock.
|
|
_CLOCK = """\
|
|
<circle cx="512" cy="512" r="244"/>
|
|
<path d="M 512 512 L 512 350"/>
|
|
<path d="M 512 512 L 636 512"/>"""
|
|
|
|
# Shield outline guarding filled cutlery. The fork is paired with a knife on
|
|
# purpose: a lone three-tine fork reads as a trident at small sizes.
|
|
_SHIELD_CUTLERY = """\
|
|
<path d="M 512 275 L 752 366 L 752 544 C 752 655 646 723 512 753 \
|
|
C 378 723 272 655 272 544 L 272 366 Z"/>
|
|
<g transform="translate(356,362) scale(13)" fill="{{ACCENT}}" stroke="none">
|
|
<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03\
|
|
C11.34 12.84 13 11.12 13 9V2h-2v7z"/>
|
|
<path d="M16 12h2.5v10H21V2c-2.76 0-5 2.24-5 5v5z"/>
|
|
</g>"""
|
|
|
|
# Two ticked-off list rows. More specific to a notes/todo app than a lone
|
|
# checkmark, and still legible at 48dp.
|
|
_CHECKLIST = """\
|
|
<path d="M 272 372 L 346 446 L 478 314"/>
|
|
<path d="M 594 400 L 752 400"/>
|
|
<path d="M 272 620 L 346 694 L 478 562"/>
|
|
<path d="M 594 648 L 752 648"/>"""
|
|
|
|
# Two interlocking stadium (pill-ring) outlines, both rotated -45 deg and
|
|
# offset along the anti-diagonal so their strokes overlap in the middle --
|
|
# reads as both "habit-stacking" (linked items) and "don't break the chain".
|
|
# Ring height is tall enough that the 72px stroke leaves a genuinely hollow
|
|
# interior (height - 2*stroke-width well above MIN_NEGATIVE_SPACE); a
|
|
# shorter ring's stroke fills the interior solid and reads as a blob.
|
|
_CHAIN_LINK = """\
|
|
<g transform="translate(467,557) rotate(-45) translate(-150,-110)">
|
|
<rect x="0" y="0" width="300" height="220" rx="110" ry="110"/>
|
|
</g>
|
|
<g transform="translate(557,467) rotate(-45) translate(-150,-110)">
|
|
<rect x="0" y="0" width="300" height="220" rx="110" ry="110"/>
|
|
</g>"""
|
|
|
|
|
|
# An isometric storage carton: hexagonal silhouette plus the three edges that
|
|
# meet at the near-top corner (one down the front, two up the top face).
|
|
#
|
|
# A front-on rectangle with a lid seam was tried first and rejected — a seam
|
|
# plus a divider reads as a browser window or a table header, not a box. The
|
|
# isometric form has no such collision anywhere else in the family, and it is
|
|
# the shape people already recognise as "a package of stuff".
|
|
#
|
|
# Geometry: a regular hexagon, centre (512,512), circumradius 240, pointy top
|
|
# and bottom. Opposite faces sit 208px apart, leaving 136px of hollow once the
|
|
# 72px stroke is drawn — comfortably past MIN_NEGATIVE_SPACE, so no face fills
|
|
# in at 48dp. Rendered ink is 488x552, inside SAFE_BOX on both axes.
|
|
_STORAGE_BOX = """\
|
|
<path d="M 512 272 L 720 392 L 720 632 L 512 752 L 304 632 L 304 392 Z"/>
|
|
<path d="M 512 512 L 512 752"/>
|
|
<path d="M 512 512 L 304 392"/>
|
|
<path d="M 512 512 L 720 392"/>"""
|
|
|
|
|
|
# A till receipt with a torn bottom edge and a perforation down the middle:
|
|
# one bill, two shares.
|
|
#
|
|
# Filled rather than stroked, which is what makes the tear possible at all. A
|
|
# stroked receipt cannot have a fine ragged edge: at the family's 72px stroke,
|
|
# a zigzag needs ~200px pitch to keep adjacent up-strokes MIN_NEGATIVE_SPACE
|
|
# apart, and two teeth that coarse read as a ribbon or a pair of bookmarks,
|
|
# not as a torn receipt. A filled silhouette is not bound by that rule, so the
|
|
# teeth can be small enough to read as a real tear — the "prefer a filled
|
|
# silhouette when a stroked outline reads ambiguously" case.
|
|
#
|
|
# The split is a perforation, not a divider. A full-height divider cuts the
|
|
# silhouette into two separate shapes and the receipt read is lost (the same
|
|
# bookmarks failure); a dotted tear line says "split here" while leaving one
|
|
# intact receipt. Drawn with fill-rule="evenodd" so the dots are true holes,
|
|
# not charcoal-painted ones — icon_foreground and icon_monochrome are rendered
|
|
# on transparency, where a painted hole would show as a blob and, on the
|
|
# monochrome layer, in the wrong colour entirely.
|
|
#
|
|
# Geometry: body x 312..712, y 282..752, six teeth 46px deep off a root at
|
|
# y=706. Four holes of r=30 on the centreline at y=350/446/542/638. Every gap
|
|
# is at the family budget: 36px of accent between holes (96px pitch, 60px
|
|
# diameter), 38px above the first and below the last. Ink is 400x470, inside
|
|
# SAFE_BOX on both axes.
|
|
_RECEIPT_SPLIT = """\
|
|
<path fill="{{ACCENT}}" fill-rule="evenodd" stroke="none" d="\
|
|
M 312 282 L 712 282 L 712 706 L 678 752 L 645 706 L 612 752 L 578 706 \
|
|
L 545 752 L 512 706 L 478 752 L 445 706 L 412 752 L 378 706 L 345 752 \
|
|
L 312 706 Z \
|
|
M 482 350 A 30 30 0 1 0 542 350 A 30 30 0 1 0 482 350 Z \
|
|
M 482 446 A 30 30 0 1 0 542 446 A 30 30 0 1 0 482 446 Z \
|
|
M 482 542 A 30 30 0 1 0 542 542 A 30 30 0 1 0 482 542 Z \
|
|
M 482 638 A 30 30 0 1 0 542 638 A 30 30 0 1 0 482 638 Z"/>"""
|
|
|
|
|
|
# A dip-pen nib: the character sheet is a thing you write on, and the game is
|
|
# set in the Romantic era, so the nib carries both the app and the period.
|
|
#
|
|
# Filled rather than stroked. A stroked almond outline plus a stroked vent hole
|
|
# leaves only ~20px of charcoal between the two strokes at the family's 72px
|
|
# weight — under MIN_NEGATIVE_SPACE, so the nib fills in solid at 48dp. Filled,
|
|
# the vent and the slit become true holes and the budget is spent on gaps
|
|
# instead of on stroke.
|
|
#
|
|
# A d20 was the obvious alternative and was rejected: this system rolls k6, and
|
|
# a 20-sided outline is a mess of short strokes at launcher size. A front-on
|
|
# die with pips collides with `storage-box` (both read as "boxy thing with
|
|
# marks"). Nothing else in the family is a tall pointed shape.
|
|
#
|
|
# The top edge is flat, not pointed. A symmetric almond — pointed at both ends
|
|
# — with a round hole in the upper half is a map pin, which is exactly what the
|
|
# first draft rendered as. The flat shoulder is where a real nib clips into the
|
|
# holder, and it is what stops the map-pin read.
|
|
#
|
|
# Geometry: shoulder (420,312)-(604,312), waist bowing out to ~x=400/624 around
|
|
# y=456, converging to the tip at (512,812) — ink 224x500, inside SAFE_BOX on
|
|
# both axes. Vent circle r=40 at (512,470) clears the outline by ~61px; the
|
|
# slit stops at y=700, leaving ~41px to the outline near the tip and 70px to
|
|
# the vent above. Every gap is over the 36px budget. fill-rule="evenodd" keeps
|
|
# both holes true holes — icon_foreground and icon_monochrome render on
|
|
# transparency, where a painted hole would show as a blob and in the wrong
|
|
# colour.
|
|
_QUILL_NIB = """\
|
|
<path fill="{{ACCENT}}" fill-rule="evenodd" stroke="none" d="\
|
|
M 420 312 L 604 312 Q 626 470 596 570 L 512 812 L 428 570 \
|
|
Q 398 470 420 312 Z \
|
|
M 472 470 A 40 40 0 1 0 552 470 A 40 40 0 1 0 472 470 Z \
|
|
M 512 700 L 492 580 L 532 580 Z"/>"""
|
|
|
|
|
|
GLYPHS: Final[dict[str, Glyph]] = {
|
|
glyph.name: glyph
|
|
for glyph in (
|
|
Glyph("cloud-down", "Cloud with a download arrow", _CLOUD_DOWN),
|
|
Glyph("barbell", "Barbell with plates and collars", _BARBELL),
|
|
Glyph("clock", "Clock face with hands", _CLOCK),
|
|
Glyph("shield-cutlery", "Shield guarding a fork and knife", _SHIELD_CUTLERY),
|
|
Glyph("checklist", "Two ticked-off list rows", _CHECKLIST),
|
|
Glyph("chain-link", "Two interlocking chain links", _CHAIN_LINK),
|
|
Glyph("storage-box", "Isometric storage carton", _STORAGE_BOX),
|
|
Glyph("receipt-split", "Torn receipt with a perforated split", _RECEIPT_SPLIT),
|
|
Glyph("quill-nib", "Dip-pen nib with vent hole and slit", _QUILL_NIB),
|
|
)
|
|
}
|
|
|
|
|
|
def get_glyph(name: str) -> Glyph:
|
|
"""Look up a glyph by name.
|
|
|
|
Parameters:
|
|
name (str): Glyph identifier, e.g. ``"barbell"``.
|
|
|
|
Returns:
|
|
Glyph: The matching glyph.
|
|
|
|
Raises:
|
|
KeyError: If no glyph with that name exists. The message lists the
|
|
available names, since this is usually a CLI typo.
|
|
"""
|
|
try:
|
|
return GLYPHS[name]
|
|
except KeyError:
|
|
available = ", ".join(sorted(GLYPHS))
|
|
msg = f"unknown glyph {name!r}; available: {available}"
|
|
raise KeyError(msg) from None
|