diff --git a/.markdownlint.json b/.markdownlint.json index 4f296249..0e9c8255 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -2,5 +2,20 @@ "first-line-heading": false, "MD013": false, "MD018": false, - "MD046": false + "MD046": false, + "MD033": { + "allowed_elements": [ + "a", + "b", + "code", + "details", + "div", + "img", + "p", + "picture", + "source", + "sub", + "summary" + ] + } } diff --git a/README.md b/README.md index 4efb447d..add80465 100755 --- a/README.md +++ b/README.md @@ -1,46 +1,65 @@ -![decky-romm-sync](assets/banner.png) +
-[![CI](https://github.com/danielcopper/decky-romm-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/danielcopper/decky-romm-sync/actions/workflows/ci.yml) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) -[![Coverage](https://img.shields.io/sonar/coverage/danielcopper_decky-romm-sync?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) -[![Maintainability](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) -[![Reliability](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) -[![Security](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) -[![Downloads](https://img.shields.io/github/downloads/danielcopper/decky-romm-sync/total.svg)](https://github.com/danielcopper/decky-romm-sync/releases) + # decky-romm-sync -A [Decky Loader](https://decky.xyz/) plugin that syncs your self-hosted [RomM](https://github.com/rommapp/romm) library -into Steam as non-steam shortcuts. Games appear directly in your Steam library, launch through -[RetroDECK](https://retrodeck.net/), and keep their saves in sync across devices through your RomM server. +## Your self-hosted RomM library, in your Steam library + +[Getting Started](https://danielcopper.github.io/decky-romm-sync/user-guide/getting-started/) · +[Configuration](https://danielcopper.github.io/decky-romm-sync/user-guide/configuration/) · +[Syncing](https://danielcopper.github.io/decky-romm-sync/user-guide/syncing-your-library/) · +[Managing Games](https://danielcopper.github.io/decky-romm-sync/user-guide/managing-games/) + +[BIOS & Cores](https://danielcopper.github.io/decky-romm-sync/user-guide/bios-management/) · +[Save Sync](https://danielcopper.github.io/decky-romm-sync/user-guide/save-sync/) · +[Troubleshooting](https://danielcopper.github.io/decky-romm-sync/user-guide/troubleshooting/) + +Documentation +Release +Stars +Downloads +Requires RomM 4.9.0 or newer +License -## 📖 [Read the full documentation →](https://danielcopper.github.io/decky-romm-sync/) +
-Installation, setup, save sync, BIOS management, troubleshooting, and the architecture reference all live on the -documentation site. This README is just the quick tour. +A [Decky Loader](https://decky.xyz/) plugin that syncs your self-hosted [RomM](https://github.com/rommapp/romm) library +into Steam as non-steam shortcuts. Games appear directly in your Steam library, launch through +[RetroDECK](https://retrodeck.net/), and can keep their saves in sync across devices through your RomM server. ## Features -- **Library sync** — Pulls platforms and ROMs from your RomM server and creates Steam shortcuts, complete with cover - art, hero banners, and logos (with optional [SteamGridDB](https://www.steamgriddb.com/) artwork) -- **Save sync** — Keeps save files in sync across devices through your RomM server, with newest-wins conflict resolution - and a manual override when you need it -- **ROM downloads** — Download ROMs on demand with progress tracking and a managed download queue -- **BIOS management** — Download firmware/BIOS files from RomM for systems that need them (PSX, Dreamcast, PS2, …) -- **Game detail page** — Install status, BIOS status, and download/uninstall actions right on each game's Steam page -- **Per-platform control** — Choose exactly which platforms get synced -- **Controller friendly** — Full gamepad navigation throughout the plugin UI -- **Steam Input config** — Per-shortcut Steam Input mode (Default / Force On / Force Off) -- **RetroDECK path migration** — Move your RetroDECK installation between storage locations without re-syncing -- **RetroArch diagnostics** — Detects misconfigured input drivers that break menu navigation +- **Library sync** — Pulls the platforms and collections you pick from your RomM server into Steam as non-steam + shortcuts with RomM cover art; later runs are incremental and preview what will change before touching anything +- **SteamGridDB artwork** — Add a free [SteamGridDB](https://www.steamgriddb.com/) key to get hero banners, logos, wide + capsules and custom icons, with a manual picker for games that don't match automatically +- **Save sync** — Opt-in save syncing across devices through your RomM server, automatically before launch and after you + quit; identical saves resolve silently, and if both sides genuinely changed you decide which one wins +- **Save slots & version history** — Multiple named save profiles per game, plus per-file version history with restore +- **ROM downloads** — Download on demand with progress, pause/resume/cancel, and a managed queue +- **BIOS management** — Per-platform BIOS status, download all or only what your active core requires, hash-verified + against a bundled registry, and delete them again when you're done +- **Game detail page** — Replaces Steam's page for synced games: RomM metadata, RetroAchievements progress, playtime, + install and BIOS status, save management, and per-game actions +- **Multi-disc & multi-version** — Pick the disc for multi-disc games and switch between regions or revisions of the + same game, right from its Steam page +- **Emulator cores** — Set the core per system, or override it for a single game +- **Steam Input** — Pick a Steam Input mode (Default / Force On / Force Off) and apply it to every shortcut the plugin + created +- **RetroArch input fix** — Spots the `input_driver` value that breaks controller navigation in RetroArch's menus and + repairs it in one tap +- **Follows RetroDECK moves** — Moved RetroDECK to another drive? The ROMs, BIOS files and saves the plugin manages are + relocated and your shortcuts repointed — nothing needs re-downloading +- **Cleanup tools** — Remove shortcuts per platform or all at once, uninstall ROMs, and clear orphaned grid images ## Screenshots -| QAM panel | Game detail page | -| :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------: | -| ![RomM Sync QAM panel](assets/screenshot-qam.jpg) | ![Game detail page with metadata](assets/screenshot-game-detail.jpg) | -| **BIOS management** | **Per-game actions** | -| ![BIOS status showing a required file to download](assets/screenshot-bios.jpg) | ![RomM actions menu: sync saves, download BIOS, refresh artwork](assets/screenshot-actions.jpg) | +| Quick Access panel | Game detail page | +| :----------------------------------------------: | :----------------------------------------------------: | +| ![Quick Access panel](assets/screenshot-qam.jpg) | ![Game detail page](assets/screenshot-game-detail.jpg) | +| **BIOS management** | **Per-game actions** | +| ![BIOS status](assets/screenshot-bios.jpg) | ![Actions menu](assets/screenshot-actions.jpg) | ## Requirements @@ -52,7 +71,8 @@ documentation site. This README is just the quick tour. ## Installation -### From the Decky Store +
+From the Decky Store — not available yet > ⚠️ **Not available yet.** The plugin will be submitted to the [Decky Store](https://plugins.deckbrew.xyz/) with the > **v1.0** release. Until then, use the manual install below. @@ -60,7 +80,10 @@ documentation site. This README is just the quick tour. Once published, install it straight from Decky's built-in store — open the Quick Access Menu → **Decky** → store icon, search for **RomM Sync**, and install. No Developer Mode required. -### From ZIP or URL +
+ +
+From ZIP or URL — the current method This is the current method while v1.0 is in progress. It requires **Developer Mode** in Decky Loader (Decky settings → gear icon → toggle **Developer Mode**). @@ -70,6 +93,8 @@ gear icon → toggle **Developer Mode**). 2. In Decky settings → **Developer** tab → **Install Plugin from ZIP** (or **from URL** with the [latest release link](https://github.com/danielcopper/decky-romm-sync/releases/latest/download/decky-romm-sync.zip)) +
+ > Full step-by-step instructions, including first-time setup, are in > [Getting Started](https://danielcopper.github.io/decky-romm-sync/user-guide/getting-started/). @@ -93,6 +118,13 @@ Build from source, run the tests, and read the architecture reference on the doc UI into a windowed Big Picture on the Deck, no Game Mode switching - [Backend architecture](https://danielcopper.github.io/decky-romm-sync/architecture/backend-architecture/) +[![CI](https://github.com/danielcopper/decky-romm-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/danielcopper/decky-romm-sync/actions/workflows/ci.yml) +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) +[![Coverage](https://img.shields.io/sonar/coverage/danielcopper_decky-romm-sync?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) +[![Maintainability](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) +[![Reliability](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) +[![Security](https://sonarcloud.io/api/project_badges/measure?project=danielcopper_decky-romm-sync&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=danielcopper_decky-romm-sync) + ## Acknowledgments This plugin stands on the shoulders of some great projects: @@ -116,4 +148,5 @@ This plugin stands on the shoulders of some great projects: ## License -GPL-3.0 +GPL-3.0. This is an independent project and is not affiliated with, endorsed by, or sponsored by the RomM project or +Valve Corporation. diff --git a/assets/banner.png b/assets/banner.png deleted file mode 100644 index 73933255..00000000 Binary files a/assets/banner.png and /dev/null differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..f6970acc Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100644 index 00000000..57709c3e --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/assets/store_image.png b/assets/store_image.png index f8534422..1daf5d3b 100644 Binary files a/assets/store_image.png and b/assets/store_image.png differ diff --git a/docs/assets/banner.png b/docs/assets/banner.png deleted file mode 100644 index 73933255..00000000 Binary files a/docs/assets/banner.png and /dev/null differ diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 00000000..f6970acc Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..57709c3e --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/docs/index.md b/docs/index.md index e7ec3cd0..fb8e2f27 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -![decky-romm-sync](assets/banner.png) +

# decky-romm-sync diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..1e8d8a2d --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,20 @@ +/* Site palette, taken from the plugin mark: a deep Steam-blue shell with a + lighter blue for interactive accents. Material's named palettes have nothing + in this range, so both schemes are set as custom colours. */ + +[data-md-color-scheme="default"] { + --md-primary-fg-color: #2d5876; + --md-primary-fg-color--light: #4795c9; + --md-primary-fg-color--dark: #1b3549; + --md-accent-fg-color: #2a6f9e; + --md-accent-fg-color--transparent: #2a6f9e1a; +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #1a3549; + --md-primary-fg-color--light: #2d5876; + --md-primary-fg-color--dark: #12222f; + --md-accent-fg-color: #66c0f4; + --md-accent-fg-color--transparent: #66c0f41a; + --md-typeset-a-color: #8fd0f7; +} diff --git a/mkdocs.yml b/mkdocs.yml index fc110082..05cdefcd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: decky-romm-sync -site_description: Decky Loader plugin to sync your RomM library to Steam as non-steam shortcuts +site_description: A Decky Loader plugin that syncs your self-hosted RomM library into Steam site_url: https://danielcopper.github.io/decky-romm-sync/ # Architectural Decision Records (`docs/adr/`) are kept in the repo for @@ -15,18 +15,20 @@ edit_uri: edit/main/docs/ theme: name: material + logo: assets/logo.svg + favicon: assets/logo.png palette: - media: "(prefers-color-scheme: light)" scheme: default - primary: deep purple - accent: amber + primary: custom + accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: deep purple - accent: amber + primary: custom + accent: custom toggle: icon: material/brightness-4 name: Switch to light mode @@ -39,6 +41,9 @@ theme: - content.action.edit - content.code.copy +extra_css: + - stylesheets/extra.css + plugins: - search diff --git a/scripts/logo/README.md b/scripts/logo/README.md new file mode 100644 index 00000000..308355c7 --- /dev/null +++ b/scripts/logo/README.md @@ -0,0 +1,31 @@ +# Logo + +`gen.py` draws the plugin mark — a handheld's right-hand half set in a disc, split along a 45° facet that runs unbroken +across disc, body, screen and every control. + +## Rendering + +```sh +python3 gen.py > sheet.svg +rsvg-convert -w 1300 sheet.svg -o preview.png +``` + +The sheet shows the candidate palettes side by side, each on a dark ground (plugin store, quick-access menu) and a light +one (docs site), plus 64px and 32px so small-size legibility stays visible while tweaking. + +## Changing things + +- **Palettes** — `PALETTES`, one row per candidate: disc pair, body pair, screen pair, and which optional details to + include. Every pair is (above-left, below-right) for the facet. +- **Positions and sizes** — the block of `*_GEOM` constants. These are on a millimetre grid mapped into drawing units by + `MM`, `X0` and `Y0`, so moving something means changing a measurement rather than guessing at coordinates. +- **Outline and groove** — `deck_path.txt` and `seam_path.txt`, plain SVG path data. Regenerate them only if the shape + itself changes; day-to-day work does not touch them. + +## Notes + +Two things are deliberately drawn heavier than their real proportions, because at icon sizes a faithful version +disappears: the rim inside the thumbstick, and the groove parting the grip (`SEAM_W`). + +Controls are sized by their caps, not by the recesses they sit in. The recess is noticeably wider and makes the mark +look clumsy — the thumbstick is the clearest case, where the difference is a third of the diameter. diff --git a/scripts/logo/deck_path.txt b/scripts/logo/deck_path.txt new file mode 100644 index 00000000..1f5521b1 --- /dev/null +++ b/scripts/logo/deck_path.txt @@ -0,0 +1 @@ +M 8.00 39.48 L 127.32 39.48 L 138.42 40.03 L 141.26 40.30 L 145.39 41.14 L 147.97 42.03 L 150.23 43.25 L 152.58 45.16 L 154.57 47.49 L 156.13 50.06 L 157.07 52.39 L 157.81 55.23 L 158.24 59.10 L 158.75 77.16 L 158.68 83.61 L 158.81 85.68 L 158.81 94.71 L 158.29 106.84 L 158.29 114.84 L 157.49 124.90 L 156.74 130.32 L 155.86 134.97 L 155.05 138.32 L 154.12 141.16 L 152.66 144.52 L 151.34 146.84 L 149.90 148.83 L 147.84 151.12 L 146.53 152.32 L 144.13 154.10 L 141.90 155.30 L 139.35 156.37 L 134.57 157.78 L 130.55 159.27 L 127.32 159.74 L 121.13 160.27 L 120.87 160.52 L 89.65 160.52 L 89.39 160.75 L 88.87 160.77 L 62.55 160.77 L 62.03 160.75 L 61.77 160.52 L 45.77 160.52 L 8.00 160.52 Z \ No newline at end of file diff --git a/scripts/logo/gen.py b/scripts/logo/gen.py new file mode 100644 index 00000000..deab7861 --- /dev/null +++ b/scripts/logo/gen.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +"""Generates the plugin mark: a handheld's right-hand half, set in a disc. + +Disc, body, display and every control are split along the anti-diagonal +(x + y = 200), so the facet edge runs unbroken across the whole mark, lighter +above-left and darker below-right. Emits an SVG contact sheet of the candidate +palettes on both a dark and a light ground, plus 64px and 32px sizes. + +The outline and the grip groove are stored as ready-made paths next to this +file. Everything else is laid out on a millimetre grid and mapped into the +drawing through one constant, so a position can be nudged by changing a number +that still means something. +""" + +import json +import pathlib + +SHEET_BG = "#0e1116" +LIGHT_BG = "#e9eef3" + +_HERE = pathlib.Path(__file__).parent +BODY = (_HERE / "deck_path.txt").read_text().strip() +LOWER_RIGHT = "300,-100 300,300 -100,300" + +# The millimetre grid the inner geometry is laid out on. Anchored so that a +# 117mm-tall body spans 39.46..160.80 in drawing units (1.0371 units/mm) and its +# right edge, at 149mm, lands on 158.80. +MM = 1.0371 +X0, Y0 = 4.27, 100.13 + + +def mmx(v): + return X0 + MM * v + + +def mmy(v): + return Y0 - MM * v + + +def mm_rect(x_mm, y_mm, w_mm, h_mm, r_mm): + """Centre-based rect in mm -> logo-space (x, y, w, h, rx).""" + return ( + mmx(x_mm - w_mm / 2), + mmy(y_mm + h_mm / 2), + w_mm * MM, + h_mm * MM, + r_mm * MM, + ) + + +# Screen 173.7 x 106.7, centred; its left half runs out of the disc and clips. +# Every control is sized by its cap, not by the recess it sits in — the recess +# is about 0.3mm wider all round and reads visibly too large. +DSP_GEOM = mm_rect(0, 0, 173.7, 106.7, 3.0) +PAD_GEOM = mm_rect(105.785, 1.335, 32.5, 32.5, 4.375) +DOTS_GEOM = mm_rect(97.655, -24.09, 16.25, 6.5, 3.25) +# Thumbstick. The cap is r=9.309; the moulded recess around it is r=14.419 and +# is deliberately not drawn, since it reads as an oversized stick. The ring is +# the rim of the cap's dished top, drawn heavier than life so it survives small. +STK_GEOM = (mmx(102.83), mmy(34.50), 9.309 * MM) +STK_RING_OUT = (STK_GEOM[0], STK_GEOM[1], 6.55 * MM) +STK_RING_IN = (STK_GEOM[0], STK_GEOM[1], 5.35 * MM) +RING_T = ("#1b3549", "#0e2233") + +# The groove parting the grip. Runs edge to edge, from the right-hand edge round +# to the bottom. Real width is well under a millimetre, which would be a hairline +# at icon sizes, so the stroke is drawn heavier. +SEAM = (_HERE / "seam_path.txt").read_text().strip() + + +# The wordmark is stored as outlines so no font has to be present to render it. +# It is loaded on demand because it depends on the project name, which the mark +# itself does not. +def _wordmark(): + return ( + (_HERE / "wordmark_path.txt").read_text().strip(), + json.loads((_HERE / "wordmark_meta.json").read_text()), + ) + + +SEAM_T = ("#c6d9e8", "#a6bfd4") # same light pair as the controls +SEAM_W = 1.0 + +# Menu key. +MENU_GEOM = mm_rect(115.125, 51.85, 9.52, 4.23, 2.115) + +# Speaker grille: slots, not holes. Each row is given as end-cap pairs, so the +# rows read 4 / (1 long + 2) / 3 openings. Rows run shorter towards the bottom, +# following the shell's edge. +_SLOT_R = 0.847 +_SLOT_ROWS = [ + (-34.03, [(90.18, 92.412), (94.647, 96.879), (99.115, 101.346), (103.582, 105.814)]), + (-37.23, [(90.18, 94.646), (96.882, 99.113), (101.349, 103.581)]), + (-40.44, [(90.18, 92.413), (94.648, 96.88), (99.116, 101.347)]), +] +SPEAKER_GEOMS = [ + mm_rect((x1 + x2) / 2, y, (x2 - x1) + 2 * _SLOT_R, 2 * _SLOT_R, _SLOT_R) + for y, slots in _SLOT_ROWS + for x1, x2 in slots +] + +DISC_C = ("#aec6da", "#93b0c8") +DISC_D = ("#94b0c8", "#7a99b4") +BODY_T = ("#2d5876", "#1a3549") +DSP_DARK = ("#12222f", "#0a1621") +DSP_LIGHT = ("#d3e3ef", "#b0c8db") +PAD_T = ("#c6d9e8", "#a6bfd4") + +_ALL = ("menu", "spk") + +# The chosen mark. +FINAL = ("final", DISC_C, BODY_T, DSP_DARK, _ALL) + +# Kept so alternatives stay one edit away rather than a re-derivation. +PALETTES = [ + FINAL, + ("disc darker", DISC_D, BODY_T, DSP_DARK, _ALL), + ("screen light", DISC_C, BODY_T, DSP_LIGHT, _ALL), + ("both", DISC_D, BODY_T, DSP_LIGHT, _ALL), +] + + +def _rect(geom, fill=None): + x, y, w, h, r = geom + attrs = f'x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}"' + return f'' if fill else f"" + + +def _circle(geom, fill=None): + cx, cy, r = geom + attrs = f'cx="{cx}" cy="{cy}" r="{r}"' + return f'' if fill else f"" + + +# ABXY diamond, sized by the button cap. +ABXY_GEOMS = [ + (mmx(x), mmy(y), 4.366 * MM) for x, y in ((131.71, 48.34), (122.95, 39.59), (140.42, 39.59), (131.71, 30.84)) +] + + +def _faceted(uid, geom, tones, shape=_rect): + """A shape carrying the same diagonal split as everything else.""" + return ( + f'{shape(geom)}' + f"{shape(geom, tones[0])}" + f'' + f'' + f"" + ) + + +def _seam(uid): + """The groove, stroked, carrying the diagonal split like every filled shape.""" + stroke = f'stroke-width="{SEAM_W}" fill="none" stroke-linecap="round" stroke-linejoin="round"' + return ( + f'' + f'' + f'' + f'' + f"" + ) + + +def mark(uid, pal): + _, disc, body, dsp, extras = pal + parts = [ + _seam(uid), + _faceted(f"dsp{uid}", DSP_GEOM, dsp), + _faceted(f"pad{uid}", PAD_GEOM, PAD_T), + _faceted(f"dot{uid}", DOTS_GEOM, PAD_T), + _faceted(f"stk{uid}", STK_GEOM, PAD_T, _circle), + _faceted(f"rgo{uid}", STK_RING_OUT, RING_T, _circle), + _faceted(f"rgi{uid}", STK_RING_IN, PAD_T, _circle), + ] + parts += [_faceted(f"ab{i}{uid}", g, PAD_T, _circle) for i, g in enumerate(ABXY_GEOMS)] + if "menu" in extras: + parts.append(_faceted(f"mnu{uid}", MENU_GEOM, PAD_T)) + if "spk" in extras: + parts += [_faceted(f"sp{i}{uid}", g, PAD_T) for i, g in enumerate(SPEAKER_GEOMS)] + inner = "".join(parts) + return ( + f"" + f'' + f'' + f"" + f'' + f'' + f'' + f'' + f'' + f'' + f"" + f'' + f"{inner}" + f"" + f"" + ) + + +def sheet(): + cols, tile, gap, pad = len(PALETTES), 200, 46, 40 + w = pad * 2 + cols * tile + (cols - 1) * gap + h = 660 + p = [ + f'', + f'', + f'', + ] + for i, pal in enumerate(PALETTES): + x = pad + i * (tile + gap) + p.append(f'{mark(f"d{i}", pal)}') + p.append( + f'{pal[0]}' + ) + p.append(f'{mark(f"l{i}", pal)}') + for j, (sz, ox) in enumerate(((64, 30), (32, 120))): + p.append(f'{mark(f"s{i}{j}", pal)}') + p.append( + f'' + f"oben dunkler Grund · Mitte heller Grund · unten 64 / 32 px" + ) + p.append("") + return "\n".join(p) + + +# Lockup: mark above, wordmark below. The wordmark is set a little wider than +# the disc so the pair reads as one block rather than a circle with a caption. +LOCK_PAD = 10.0 +LOCK_GAP = 26.0 +WM_TO_DISC = 1.35 # wordmark width relative to the disc's diameter + + +def lockup(pal=FINAL, ink="#1b3549", size=None): + wordmark, _WM = _wordmark() + disc_d = 168.0 + wm_w = disc_d * WM_TO_DISC + s = wm_w / (_WM["bbox"][2] - _WM["bbox"][0]) + cap = -_WM["bbox"][1] * s + w = wm_w + 2 * LOCK_PAD + h = LOCK_PAD + disc_d + LOCK_GAP + cap + LOCK_PAD + mark_dx = w / 2 - 100.0 + mark_dy = LOCK_PAD - 16.0 # the disc starts at y=16 inside the 200-unit mark + base_y = LOCK_PAD + disc_d + LOCK_GAP + cap + wm_x = LOCK_PAD - _WM["bbox"][0] * s + width = f' width="{size}"' if size else "" + return ( + f'' + f'{mark("lk", pal)}' + f'' + f'' + ) + + +def standalone(pal=FINAL, size=512): + """The mark on its own, transparent outside the disc — the shipped asset.""" + return ( + f'{mark("m", pal)}' + ) + + +if __name__ == "__main__": + import sys + + if "--lockup" in sys.argv: + ink = sys.argv[sys.argv.index("--lockup") + 1] + print(lockup(ink=ink)) + elif "--asset" in sys.argv: + print(standalone()) + else: + print(sheet()) diff --git a/scripts/logo/preview.png b/scripts/logo/preview.png new file mode 100644 index 00000000..1fd893b9 Binary files /dev/null and b/scripts/logo/preview.png differ diff --git a/scripts/logo/seam_path.txt b/scripts/logo/seam_path.txt new file mode 100644 index 00000000..83b67a57 --- /dev/null +++ b/scripts/logo/seam_path.txt @@ -0,0 +1 @@ +M 158.80 81.48 L 158.52 81.48 L 158.06 81.48 L 157.37 81.48 L 156.45 81.48 L 155.32 81.48 L 154.09 81.48 L 152.86 81.48 L 151.66 81.48 L 150.41 81.48 L 148.93 81.48 L 146.88 81.48 L 146.01 81.48 L 145.26 81.54 L 144.44 81.73 L 143.71 82.01 L 143.05 82.38 L 142.46 82.82 L 141.93 83.34 L 141.46 83.94 L 141.06 84.63 L 140.76 85.40 L 140.57 86.23 L 140.50 86.98 L 140.50 88.37 L 140.50 90.77 L 140.50 92.77 L 140.50 95.51 L 140.50 99.07 L 140.50 102.49 L 140.50 108.14 L 140.49 109.74 L 140.30 111.92 L 139.87 114.07 L 139.21 116.16 L 138.33 118.16 L 137.24 120.06 L 136.67 120.89 L 135.35 122.77 L 133.81 124.97 L 132.38 127.02 L 130.82 129.25 L 128.49 132.57 L 126.06 136.04 L 124.29 138.57 L 120.83 143.51 L 118.35 147.06 L 116.61 149.55 L 115.35 151.34 L 114.42 152.67 L 113.66 153.76 L 113.05 154.62 L 112.82 154.96 L 112.36 155.61 L 111.92 156.25 L 111.72 156.52 L 111.39 157.06 L 111.10 157.66 L 110.88 158.27 L 110.73 158.83 L 110.65 159.25 L 110.61 159.60 L 110.59 159.72 L 110.58 159.97 L 110.57 160.19 L 110.57 160.30 L 110.57 160.52 L 110.57 160.68 L 110.57 160.74 L 110.57 160.42 L 110.57 160.26 L 110.57 160.26 L 110.57 160.26 \ No newline at end of file