BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / design.md

design.md

design.md 42.2 KiB Download Raw
1# Paperdoll Renderer — design
2
3**Status: design recorded with a foundation shell only.**
4Prepared 6 November 2026 (Europe/Amsterdam). Requirements originate with Tim Falken. Sections explicitly marked **proposal** remain implementation decisions to validate; the verbatim appendix is authoritative for user intent. The Rust program shipped with this design is the section 12 step 0 shell: it compiles, opens a status window, and tells the truth about what is not built. It does not import models and it does not render paperdolls. Synthetic test bodies, when present under `tests/fixtures/bodies/`, are fixtures for later features; this program does not load them.
5
6Anonymous HTTP requests to `https://git.timfalken.com/groups/golden-age` and `https://git.timfalken.com/golden-age/paperdoll-renderer` returned 404. BlueGit also answers 404 when a private project is not visible to the caller, so those responses do not prove the private repository is absent. This workspace does not create the group or the repository. The requested BlueGit destination stays group **Golden Age**, slug `golden-age`, repository **Paperdoll Renderer**, slug `paperdoll-renderer`.
7
8## 1. Product, scope and non-goals
9
10Build a native Rust desktop asset-authoring application for rendering animated human bodies and bone-attached equipment into aligned 2D paperdoll PNG sequences. A project contains male and female source models, their imported animation inventories, equipment presets, render settings, output paths and output metadata. Either model slot may remain empty while authoring. Missing models must be reported before a render starts, not replaced by invented characters.
11
12The application has three distinct experiences: a project/model workspace, an interactive **3D attachment editor**, and a **rendered 2D paperdoll preview**. The latter must consume actual exported maps; it must not disguise a 3D viewport as a finished sprite renderer.
13
14The later game character-renderer/controller is OUT OF SCOPE. The in-tool compositing preview is in scope because it verifies asset compatibility. No bundled commercial models, automatic retargeting, physics-based cloth, equipment deformation/weight painting, game controllers or multiplayer are promised. Equipment is initially rigid geometry attached to bones. Imported skinned equipment can be a later explicitly scoped extension.
15
16BlueGit destination requested: group **Golden Age**, proposed slug `golden-age`; repository **Paperdoll Renderer**, slug `paperdoll-renderer`. Check existence before creating either. Private visibility is a provisional safe default, not a claim about an existing group's settings. Do not change unrelated group/repository access.
17
18## 2. Requirements and traceability
19
20| ID | Requirement | Verification |
21|---|---|---|
22| R01 | Rust application with user-selected male/female models | Open/save missing and populated slots; later manual desktop smoke test |
23| R02 | Discover all supported embedded animation clips | Synthetic licensed fixture inventory, including unnamed/duplicate clips |
24| R03 | Body and equipment modes | Export body plus isolated equipment from identical scene state |
25| R04 | Three pitches times twelve yaw positions | Exactly 36 unique orientations; yaw starts facing viewer |
26| R05 | Initial geometry render at 8x target dimensions and configured FPS | Explicit render manifest and dimension/frame-count tests |
27| R06 | Edge/detail-guided downsampling | Image fixtures retain thin details better than baseline without alpha fringes |
28| R07 | Target-resolution diffuse, depth, normal maps | Round-trip decoding, finite normalized normals and shared depth contract |
29| R08 | Subtle diffuse edge highlights from depth/normals | Deterministic fixture with bounded highlight effect |
30| R09 | Required filename structure | Golden path examples plus sanitization/collision tests |
31| R10 | Bone-relative XYZ position and rotation with live orbitable 3D preview | Transform-chain unit tests and manual viewport test |
32| R11 | Binary equipment presets embedding models and attachment metadata | Round-trip, version/truncation/resource-limit tests |
33| R12 | Equipment set name lowercase without spaces | Validation; reject reserved body name and path traversal |
34| R13 | Equipment-only rendering with shinymap | No body occlusion/write contamination; specular fixture |
35| R14 | Folder plus .ppd manifest describing assets, presets, outputs | Portable relative paths and safe atomic saves |
36| R15 | 2D looping animation, sex/equipment/clip selection and 36-angle navigation | State-machine and depth-compositing tests |
37| R16 | Held light-control buttons rotate continuously in degrees | Delta-time tests including key/button release/focus loss |
38| R17 | Per-feature branch, tests, non-FF master merge, no initial MRs | Two-parent merge topology and CI checks |
39| R18 | Tag tests gate AppImage, deb, Arch package, Windows build and release | Packaging smoke tests and complete-artifact release gate |
40| R19 | Minimum import goals OBJ/MTL/textures and FBX; broader support where feasible | Format matrix and fixture-based import tests |
41
42## 3. Proposed architecture and stack
43
44**Proposal, not compatibility certification:** use Bevy for windowing, scene graph, skeletal animation, glTF loading and GPU rendering (wgpu), with a compatible bevy_egui integration for authoring panels. Choose and pin compatible published versions when implementation starts; do not guess latest-version APIs or dependency compatibility. Use ordinary Rust libraries with no GUI/GPU requirements for project validation, naming, render schedules, transforms and image processing. Tests for this core must run headlessly.
45
46A dependency-minimal executable skeleton may precede the full viewport; it must say which capabilities are unimplemented. An application that only prints a placeholder message is not the completed desktop renderer. Rust edition/toolchain must be explicit, version-pinned after confirming installed/build-runner availability. Commit Cargo.lock for reproducibility.
47
48Proposed workspace boundaries:
49- `crates/ppd-core`: project types, validation, portable paths, render schedule, filename codec and format versions.
50- `crates/ppd-import`: import adapters, coordinate normalization, normalized assets and animation discovery.
51- `crates/ppd-render`: shared camera, scene sampling, offscreen maps and readback.
52- `crates/ppd-image`: linear-color downsampling, detail extraction, edge highlight and PNG codecs.
53- `crates/ppd-preview`: deterministic depth compositing and preview/light state independent of UI where possible.
54- `crates/paperdoll-renderer`: desktop shell, model/preset editors, render progress and preview tabs.
55- `tests/fixtures`: tiny synthetic or permissively licensed fixtures, with provenance and licenses.
56- `packaging` and `scripts`: verified platform packaging and CI helpers.
57
58Do not create empty crates solely to imply completion. Establish boundaries as features land. Rendering jobs operate on immutable validated snapshots so editing a project cannot silently change an active batch. Worker messages report progress and cancellation; UI/GPU ownership stays on the appropriate thread.
59
60## 4. Import format advice and support matrix
61
62**Recommendation:** glTF 2.0/GLB is the preferred canonical interchange for animated characters. GLB is convenient for distributing a self-contained body and avoids accidental texture-path breakage. Preserve original sources and conversion/import provenance. OBJ and FBX are minimum product goals, not necessarily native Bevy capabilities. A Bevy glTF loader must not be advertised as an FBX or OBJ loader.
63
64| Format | Mesh | Materials/textures | Skeleton | Animation | Intended role / status |
65|---|---|---|---|---|---|
66| glTF 2.0 .gltf/.glb | Yes | PBR metallic/roughness and referenced/embedded textures | Skins/joints | Node/skeletal clips; morph support must be tested | Preferred character and equipment path; native Bevy glTF adapter planned |
67| OBJ + MTL + texture files | Yes | MTL subset with explicit conversion and relative textures | No | No skeletal clips | Required static equipment/import preview; cannot by itself satisfy animated body requirement |
68| FBX ASCII/binary | Yes | Varies; embedded/external textures and legacy material conversion | Commonly | Commonly multiple clips/takes | Required adapter/conversion path with compatibility fixtures; never claim universal FBX fidelity |
69| Collada .dae | Usually | Adapter-dependent | Possible | Possible | Optional Assimp-backed candidate, not guaranteed until fixtures pass |
70| PLY / STL | Static geometry | PLY colors possible; STL no conventional textures | No | No | Optional static equipment only; low priority |
71| .blend | Not a portable interchange contract | Requires Blender/version matching | Possible through export | Possible through export | No direct minimum support; explicit external export to GLB preferred |
72
73FBX/OBJ adapter decision remains **open**: evaluate Assimp via maintained Rust bindings (e.g. russimp, native Assimp build dependency), dedicated format loaders, or explicit Blender conversion. Assimp offers a broad-format normalization route but its Rust binding/native ABI, packaging, material conversion, skin weights and animation correctness must be proven on Linux AND Windows. A native library must be bundled/licensed correctly or statically linked where permitted. Do not silently require an installed Blender; if used as an optional converter, show the requirement and invoke it only after explicit user selection. Avoid a heavyweight runtime dependency when an in-process adapter passes tests.
74
75Import normalizes axes, handedness, units, winding, inverse-bind matrices and texture color spaces ONCE, then retains conversion metadata. Detect singular transforms, invalid weights, missing bones/textures and unsupported materials. Imported file paths and embedded archives are untrusted: no traversal, arbitrary script execution or unbounded decompression. Keep parse limits and import failure diagnostics.
76
77OBJ bodies can be displayed as static geometry, but must not pretend to have a skeleton or the requested animations. A UI must clearly disable unavailable bone/clip features. Separate equipment geometry from body rig; bone matching uses stable joint identity/path, not only an ambiguous display name.
78
79Format acceptance gate: fixtures cover mesh topology, alpha materials, normal maps, external relative texture paths, one animated skinned body, multiple clips, bone-local transforms, malformed files and missing resources. A green fixture does not certify every exporter dialect.
80
81## 5. .ppd project and asset ownership
82
83**Proposal:** `.ppd` is a versioned UTF-8 JSON manifest in a project directory, with `format_version`, project identifier, display name, optional male/female model references, import transforms, animation inventory overrides, equipment preset references, render settings, output locations and generated-set metadata. Keep source references and normalized cache separate. Use project-relative paths by default; importing may copy models plus dependencies into `assets/` after user choice. External references need explicit portability warnings and relinking, never silent data loss.
84
85Suggested layout: `character.ppd`, `assets/body/`, `assets/equipment/`, `presets/`, `cache/`, `renders/`. The .ppd manifest is not the binary equipment preset. Cache is disposable and content-addressed; source and preset files are not. Save through a temporary sibling plus atomic rename, preserve valid previous state on failure, reject unsupported newer schema versions with a useful message. Unknown fields should not be silently discarded on rewrite without a documented migration policy.
86
87Validate finite numeric values, dimensions > 0, bounded dimensions and memory estimate, positive FPS, valid clip references, existing model/preset paths and output directories distinct from sources. A missing optional sex slot is a valid project, but cannot render that slot. No absolute-path leakage into exported manifests unless user explicitly chooses external references.
88
89## 6. Camera, axes, alignment and animation sampling
90
91Required views: pitches -30, 0, +30 degrees; yaw 0, 30, 60, ..., 330 degrees. **Proposal:** yaw zero means the standardized character front faces the camera, world +Y is up, and positive yaw is defined by a documented right-hand convention. Distinguish camera elevation from model rotation: use orbiting orthographic camera around fixed character origin. Golden orientation fixture must show labelled front/back/left/right to remove ambiguity before production export.
92
93Use orthographic projection for aligned layering. Shared camera framing, pivot, near/far, canvas size and unit normalization are stored per sex/export set and must match across body/equipment. Determine a stable envelope across selected animations, with configurable margin and equipment allowance. Never independently auto-frame each equipment piece or animation frame. Source-model forward-axis correction is explicit in project import settings.
94
95**Proposal:** sample looping clips at t=i/FPS for 0 <= i < ceil(duration*FPS), excluding a duplicate terminal sample; validate zero-duration clips and offer a bind/static frame instead. Store actual sample timestamps and duration in output metadata. Root motion is initially in-place with a documented extraction policy; preserve original clips and allow future explicit mode. No interpolation of ordinal frames as though every clip had the same duration.
96
97The 8x requirement means **8x width AND 8x height**, hence 64x pixel count. FPS controls animation sampling rather than interactive preview performance. Render maps in a bounded streaming pipeline, not all frames in RAM. Estimate disk/GPU/memory cost before starting: frame count times 36 views times map count. Cancellation stops between safe work units and leaves an incomplete marker, never an apparently complete asset set.
98
99## 7. Export naming and metadata
100
101Required schema: `<geslacht>-<settype>-<animatienaam>-<pitch>-<yaw>-<maptype>-f<framenummer>.png`.
102
103**Proposal:** sex tokens `male` and `female`; body set type exactly `body`; equipment name validated as a lowercase slug without spaces, reject reserved `body`. Use deterministic safe animation slugs plus collision registry preserving original names in a companion manifest. Decide delimiter escaping explicitly: negative pitch creates adjacent hyphens, so parse from the known suffix rather than naive splitting. Example `male-body-walk--30-0-diffuse-f000000.png` corresponds to pitch -30/yaw 0/frame 0. A six-digit frame width is a provisional minimum, not a cap; width and naming are stored in metadata.
104
105The companion output manifest stores original clip IDs/names, name tokens, timestamps, dimensions, FPS, camera matrices/conventions, shared depth range, normal space, alpha mode, specular convention, render settings/version, source fingerprints and completion status. This metadata supplements rather than replaces the required PNG filenames. Output writes use temporary files and explicit overwrite choice. Sanitize names without creating collisions or interpreting slashes as directories.
106
107## 8. Map contracts and rendering algorithm
108
109Diffuse is an **unlit base color** suitable for later lighting, not an already lit screenshot. Preserve alpha, use sRGB encoding only at output boundaries and process color in linear light. Transparency outside geometry remains zero; distinguish uncovered pixels from valid near-depth pixels.
110
111Depth **proposal:** 16-bit grayscale PNG representing linear camera-space depth normalized to one shared near/far interval for all compatible layers. Smaller valid values mean nearer the viewer. Use diffuse alpha/coverage as validity; never interpret the transparent background as the closest surface. No per-frame or per-equipment depth rescaling. Record the exact decode equation in metadata. Depth ordering tests must include equipment in front of and behind different body regions.
112
113Normals **proposal:** view-space geometric/shading normal, xyz encoded from [-1,1] to [0,1] in RGB PNG, with documented image Y convention; normal space must match the preview light transform. Normalize after interpolation/filtering. Uncovered pixels have alpha zero and a neutral placeholder normal. Include tangent-space source normal maps where supported and explicitly test their conversion; a geometry-normal bake is not automatically full source-material normal fidelity.
114
115Equipment shinymap **proposal:** grayscale scalar specular strength (0..1), with a documented default exponent/roughness in metadata. Filename token `shiny` is provisional because Tim named the concept but did not mandate a token spelling. Metallic/roughness and Phong materials do not map uniquely to one scalar; expose a conservative conversion/default and retain editable overrides. Do not label it physically exact PBR. Body has diffuse/depth/normal; equipment additionally shiny.
116
117Pipeline per shared scene sample/view:
1181. Evaluate skeleton and bone world transforms at the exact sample time.
1192. Render high-resolution unlit color/coverage and geometry buffers at 8x target dimensions. Depth/normals originate from the actual same surfaces, not image luminance.
1203. Compute an internal detail map using color gradients plus alpha/depth/normal discontinuities, bounded weights and transparent-neighbor handling. Detail map is intermediate, not a fourth body output map.
1214. Downsample diffuse with deterministic coverage-aware weighted reconstruction in linear premultiplied color; unpremultiply safely for straight-alpha PNG output. Tune thin-feature retention on fixtures, not on a named but unimplemented algorithm.
1225. Resolve target-resolution depth/normals from coherent contributing surfaces. Avoid mixing background with foreground or averaging front and back depths into nonphysical geometry. Align the chosen representative/coverage policy across maps. Renormalize normals.
1236. Add a subtle configurable rim/detail accent to diffuse based on normal/depth discontinuities, never a light-direction bake. Clamp magnitude, preserve alpha; default must be validated visually to avoid double-lighting later.
1247. Encode all target-resolution PNGs atomically and record checksums/metadata. Test deterministic CPU stages independently from GPU tolerance.
125
126Equipment pass hides the body from **all color, depth, normal and specular writes**, while still evaluating its rig and animating attachments. The body is not an invisible occluder; otherwise equipment behind it would be missing when layers are recomposed. Equipment self-occludes normally. Every layer must use the same projection and absolute depth contract. Hair/alpha cutout versus true blended transparency is an open fidelity decision; initial opaque/cutout support must be stated rather than promising arbitrary multilayer transparency from a single depth value.
127
128## 9. Equipment editor and binary presets
129
130UI selects sex, target skeleton/bone, imported equipment mesh, XYZ translation and XYZ Euler rotation in degrees. **Proposal:** persisted rotation is a quaternion with UI Euler values/order documented; test round-trip and avoid cumulative rotations each frame. Attachment transform is `body_world * animated_bone_world * attachment_local`; clarify inverse-bind usage in importer and test with nonidentity parents. Scale is initially asset import normalization, not an unrequested body-part editor feature.
131
132Live 3D preview displays body and selected equipment, allows free orbit/zoom and optional animation selection for placement checks. It is separate from the discrete 2D preview. Display a bone tree and highlight unresolved attachments. A preset supports multiple meshes attached to multiple bones, sex flag, stable bone paths, original/import transforms and embedded geometry/material/texture payloads.
133
134Binary preset **proposal:** magic header, schema version, bounded metadata length and asset count, checksummed entries and compressed payload chunks. Choose a documented serialization/container implementation in the preset feature; do not dump Rust memory layouts. Include rig fingerprint and reject mismatched skeletons with an actionable remapping prompt. Preserve original or normalized embedded asset representation plus license/provenance, protect against decompression bombs and traversal, save atomically. Filename extension for presets remains a proposal; .ppd is reserved for project manifests.
135
136## 10. Rendered 2D preview
137
138Select sex, animation and zero or more compatible equipment sets. Validate same project/rig/export contract and sample inventory. Play actual exported frames on a loop with elapsed-time accumulation; FPS and playback speed are explicit. Missing maps/frames show a diagnostic rather than silently using another animation.
139
140Left/right controls wrap among twelve yaw values. Up/down selects one of three pitch rows with clamping, not cyclic hidden extra angles. The orientation zero/positive direction agrees with export. Changing angle preserves animation time. Holding light buttons rotates azimuth/elevation continuously in degrees per second using delta time; releasing, focus loss or pointer cancellation stops motion. Clamp elevation/wrap azimuth with clear coordinate convention. These controls are continuous even though model view selection is discrete.
141
142Depth compositor selects the nearest valid surface per pixel from body and chosen equipment, with stable epsilon/tie-break ordering. Chosen pixel supplies diffuse, normal and shiny from the **same layer**. Blend alpha edges using a documented approximation; single-depth compositing cannot reconstruct arbitrary order-dependent translucent surfaces. A simple ambient + diffuse + specular preview proves maps and lighting; it is not the future game controller. Transform the continuous light into the same view space used by each normal map.
143
144## 11. Development workflow, quality and packaging
145
146Each feature starts on its own `feature/...` branch from current master. Implement its tests and acceptance criteria, run local checks, commit, then `git merge --no-ff` into master and push. No initial-phase MRs. Keep branch/feature and merge SHAs in the coordinator status file, report completion before the next feature. Setup/design may use `feature/project-design`; establish a baseline master commit first if required to obtain a genuine two-parent merge. Never fabricate a merge on an unborn branch.
147
148CI filename requested: `blue-ci-cd.yml`. **Unverified infrastructure detail:** do not invent BlueGit YAML syntax. Inspect documented BlueGit runner schema and narrow existing `timfa/posprint` and `legacy-cycles` pipeline/release examples before writing configuration. Respect runner/image selection, artifact retention and release API semantics. No GitLab mirror is requested for this new project.
149
150Tests run on MR events (future-proof despite no current MRs), master pushes AND tags. Quality gates: `cargo fmt --check`, `cargo check --workspace --all-targets --locked`, `cargo clippy --workspace --all-targets --locked -- -D warnings`, `cargo test --workspace --locked`; feature combinations should be covered explicitly when introduced. Pin toolchain/components and package tooling. Core tests require no GPU; separate GPU integration tests may use a supported software adapter, with explicit results rather than silent skip-as-pass.
151
152Tag pipeline graph: quality/tests → four packaging jobs → artifact verification → one release publish. A failure of ANY required artifact prevents release. Build Linux x86_64 AppImage, Debian/Ubuntu .deb, Arch .pkg.tar.zst and Windows x86_64 .exe in a .zip. Verify target architecture and contents; packaging a Linux binary into a Windows archive is not a build. Prefer native Windows runner when available, or verified mingw target/toolchain with dependency support. AppImage and distribution packages need desktop integration/icon/license, runtime library audit and version mapping from tag to package version. Include checksums and build provenance; signing is optional future work, never invent signatures.
153
154Release publication uses documented BlueGit release/artifact API and job-scoped least-privilege credentials; no tokens in source/logs/status. All assets attach only after all outputs pass verification, retries are idempotent and do not delete unrelated releases. Distinguish local packaging smoke tests from a real successful tag/release pipeline. **No tag or release is authorized during setup.** A configured future tag trigger is not permission to create a tag now.
155
156Verified BlueGit behaviour, inspected 6 November 2026, is recorded in section 15. The configured tag pipeline calls that API. With the API as it exists today, publishing the same tag that started the pipeline is rejected. That limit is part of the design record, not a claim that a release was created.
157
158## 12. Incremental feature plan and acceptance gates
159
1600. Foundation/design/CI: group and repo existence checks; private repo; single detailed design.md with verbatim appendix; minimal honest compileable Rust shell; locked dependencies/toolchain; genuine two-parent setup merge; tests and verified CI schema. No claim that rendering/import works. Test packaging scripts locally where possible without publishing.
1611. Project model and validation: .ppd open/save/new, portable paths, schema/version checks, model slots and render settings; atomic persistence/invalid paths tests.
1622. Normalized import and animation inventory: glTF/GLB fixtures first, required OBJ+MTL/textures and FBX adapter proof; format-specific truthful UI/errors and skeleton/clip inventories; integration fixture tests before declaring format support.
1633. Desktop 3D viewport and animation sampling: visible real imported model, orbit, clip controls, source-axis normalization; transform tests and manual visual test.
1644. Equipment attachments and binary presets: bone tree, sex choice, numeric offsets/rotations, live placement, embedded multi-mesh assets; round-trip and rig mismatch tests.
1655. Shared batch camera/sampling and raw buffers: 36 views, common framing, 8x geometry output/readback, diffuse/depth/normal alignment; orientation/occlusion fixture before aesthetic postprocessing.
1666. Production image pipeline: detail-aware downsampling, depth/normal resolve, rim accents, shiny conversion, filename/manifest export, progress/cancel/resume; compare actual output and memory budgets.
1677. Rendered 2D compositor preview: real exported frames, depth ordering, looping, sex/equipment/clip selection, discrete view controls and continuous held light controls; deterministic state tests and manual lighting test.
1688. Packaging/hardening: verify all platform packages and install/run behavior, imports with native dependencies, accessibility and failure recovery; actual releases only on later explicit tag authorization.
169
170Every feature is independently reported and merged non-FF only when its acceptance gate passes. If compatibility or rendering remains untested, name that limitation and do not merge a false completion claim.
171
172## 13. Open decisions and risks
173
174- Confirm target resolution/FPS defaults, output color treatment, root-motion policy, front-axis and positive-yaw convention with a labelled fixture before production content.
175- FBX exporter variants/native Assimp packaging and skeletal animation fidelity are high-risk; prototype early rather than after UI completion.
176- Eightfold dimensions impose 64x pixels; support tiling or reject unsupported render sizes with estimates, not GPU crashes.
177- Shared framing/depth/normal contracts are essential; independently normalized equipment maps cannot be depth-composited correctly.
178- Alpha-blended materials cannot be fully represented by one surface depth; document cutout/opaque MVP and approximation.
179- Bone attachment supports rigid equipment, not automatic skinning/retargeting; expose that limit.
180- glTF PBR/FBX legacy materials to a single shinymap is lossy; retain source metadata and editable conversion settings.
181- Package version mapping, Windows runner/toolchain and BlueGit group runner availability must be verified, not assumed.
182- Keep model provenance/licensing with embedded presets; importing assets does not grant redistribution rights.
183
184## 14. Research references and verification limits
185
186- Bevy project and documentation: https://bevy.org/ (preferred-stack proposal; exact dependency versions and API compatibility still require implementation verification).
187- Assimp repository: https://github.com/assimp/assimp (candidate broad-format import dependency, not universal file compatibility guarantee).
188- Rust Assimp binding docs inspected: https://docs.rs/russimp/latest/russimp/ (reported russimp 3.2.1 at inspection; no build/compatibility test performed).
189- glTF specification reference: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html (canonical interchange recommendation; implementation must test supported subsets).
190- BlueGit CI specification: `GET https://git.timfalken.com/api/v1/ci/spec` and `docs/CI.md` in `timfa/bluegit` (file name `blue-ci-cd.yml`, job fields, variables, release API).
191- Working pipelines read in public repositories: `timfa/posprint` `blue-ci-cd.yml` and `scripts/ci/release.sh`; `timfa/minecart-trains` `blue-ci-cd.yml`; `timfa/click-tween` `blue-ci-cd.yml` and `scripts/release.sh`.
192- Release implementation read in `timfa/bluegit` `internal/server/ops_ci.go` (`opPublishRelease`) and `internal/server/release_replace.go` (`checkReplace`).
193
194## 15. Verified BlueGit CI and the release-trigger limit
195
196Inspected sources, all public on 6 November 2026:
197
198| Source | What it shows |
199|---|---|
200| `GET /api/v1/ci/spec` and `timfa/bluegit` `docs/CI.md` | The pipeline file is `blue-ci-cd.yml`. Jobs may be top-level. `rules` / `only` / `needs` / `artifacts.paths` / `artifacts.expire_in` / `extends` / `resource_group` / `workflow` are real fields. `CI_COMMIT_TAG` is empty on a branch and `CI_COMMIT_BRANCH` is empty on a tag. `CI_JOB_TOKEN` exists only inside a job container. Image presets include `bluegit/base` and `bluegit/rust` (Rust 1.81). Any other image name, such as `rust:1.83-bookworm`, is a normal Docker image. |
201| `timfa/posprint` `blue-ci-cd.yml` | Tests on merge requests and on `master`. Package jobs use `needs: [test]`. Artifacts are exact file paths, not globs. The release job runs on `master` and calls `POST $CI_API_URL/repos/$CI_PROJECT_PATH/releases` with `Authorization: Bearer $CI_JOB_TOKEN`. Tag pushes are disabled with `if: $CI_COMMIT_TAG != ""` / `when: never`. The comment in that file says tag pipelines must not rebuild old tags. Windows is cross-compiled with MinGW because that comment states there is no Windows runner. |
202| `timfa/posprint` `scripts/ci/release.sh` | The script says the release API creates the tag itself. It refuses to release when `HEAD` is already tagged. |
203| `timfa/minecart-trains` `blue-ci-cd.yml` | States the GitLab project used to release on a new tag, and that on BlueGit the release API creates the tag, so tag pushes start no pipeline. |
204| `timfa/click-tween` `blue-ci-cd.yml` | A tag pipeline may build and verify. The job that publishes the release is manual on `master` and creates the tag through the API. |
205| `timfa/bluegit` `opPublishRelease` | If the tag already exists and `replace` is not set, the API returns HTTP 409 (`err.tagExists`). `replace: true` is checked by `checkReplace`, which rejects a tag pipeline (`err.replaceRef`, HTTP 403). A tag pipeline therefore cannot attach assets to the tag that started it, and cannot replace that release either. |
206| `timfa/legacy-cycles` | The project page redirects to releases and says only the releases are public. `GET /api/v1/projects/timfa/legacy-cycles/repository/tree` and the CI file returned 404. The public release assets for v0.0.38 are a Debian package, an Arch `.pkg.tar.zst`, a Windows zip, and Linux tar archives. No AppImage is listed. Its pipeline YAML was not available to copy. |
207
208Consequences for `blue-ci-cd.yml` in this repository:
209
2101. Syntax is limited to fields confirmed in the CI spec or in the pipelines above. No GitLab-only keyword is added (`pages`, `spec`, child pipelines, `id_tokens`).
2112. Tests run for merge-request pipelines, for pushes to `master`, for pushes to `main`, for `CI_DEFAULT_BRANCH`, and for tags. This workspace's default branch is `main`. Tim's existing BlueGit projects use `master`. Covering both avoids a silent skip after the project moves.
2123. On a tag push the four package jobs run only after `test`, then `verify:packages` checks magic bytes so a Linux executable is not renamed into the Windows zip. `release` needs `verify:packages`, so any failed package skips publication.
2134. `release` runs `scripts/ci/release.sh` after the four packages and `verify:packages`. That script does not call the create-release route and does not send `replace`. A second inspection of public BlueGit master (below) found no way to attach artifacts to the tag that already started the pipeline. The script exits 3 with `release-blocked: no-attach-endpoint`. That is a refused publish, not a created release. The tag order stays a tag push. No tag is created by this repository.
2145. Package scripts map a tag `vX.Y.Z` or `X.Y.Z` to package version `X.Y.Z`. Other tag spellings fail the package job instead of inventing a version.
2156. The Rust image is `rust:1.96-bookworm`. That tag is the one `timfa/posprint` already runs on BlueGit. `rust-toolchain.toml` pins the same compiler, 1.96.0, which was installed here with rustup. The machine default before that pin was 1.83.0. 1.83.0 cannot resolve the current eframe 0.29.1 graph: several transitive crates require edition 2024, which Cargo 1.83 does not parse. `bluegit/rust` is Rust 1.81 on Alpine and is the wrong image for this X11 shell. `bluegit/base` is used for the verify and release jobs, as in posprint.
2167. posprint states BlueGit does not set `CI_PROJECT_DIR` and that jobs start in the checkout. Scripts use `$PWD`.
217
218The toolchain is Rust 1.96.0, edition 2021. Edition 2021 remains explicit; the crate does not opt into edition 2024. `Cargo.lock` is committed. `cargo fmt`, `cargo check`, `cargo clippy -D warnings` and `cargo test` are the test job. No GPU test is required for the foundation shell.
219
220### Release routes that exist, and the missing attach call
221
222Rechecked against public `timfa/bluegit` master (`internal/server/api_routes.go`, `releaseRequest` in `api.go`, `opPublishRelease` in `ops_ci.go`, `checkReplace` in `release_replace.go`, `gitrepo.Tag` in `internal/gitrepo/repo.go`) and `GET /api/v1/openapi.json`. No PUT or PATCH release route exists. `releaseRequest` has `tag`, `commit`, `name`, `body`, `replace` and `assets`. There is no field that means "use the tag that is already there". `encoding/json` drops unknown fields, so a new client flag would still reach `HasTag` and return 409.
223
224| Method | Path | What it does |
225|---|---|---|
226| GET | `/api/v1/projects/{owner}/{repo}/releases` | List releases. |
227| POST | `/api/v1/projects/{owner}/{repo}/releases` | `createRelease` with a user token. `opPublishRelease` copies job artifacts, then `gitrepo.Tag` runs `git tag -a`. |
228| GET | `/api/v1/projects/{owner}/{repo}/releases/{tag}/assets/{name}` | Download one asset. |
229| DELETE | `/api/v1/projects/{owner}/{repo}/releases/{tag}` | Delete the release. Query `delete_tag=true` also deletes the git tag. |
230| DELETE | `/api/v1/projects/{owner}/{repo}/releases/{tag}/assets/{name}` | Delete one asset. The tag stays. This does not upload. |
231| POST | `/api/v1/repos/{owner}/{repo}/releases` | Same `createRelease`, and the route a job token may call. `replace: true` is allowed only by `checkReplace`: running job of this project, pipeline on an unprotected branch, not `forks/*`, actor maintainer or higher, existing release made by a job on that same branch. A tag pipeline is rejected with HTTP 403. |
232| POST | `/api/v1/projects/{owner}/{repo}/repository/tags` | Create a git tag. No release assets. |
233| GET | `/api/v1/projects/{owner}/{repo}/pipelines/{id}/jobs/{job}/artifacts` and `.../artifacts/{path}` | List or download a job artifact. Not a release. |
234
235Minimal server change, not applied in this repository: add `POST /api/v1/repos/{owner}/{repo}/releases/{tag}/assets`, job token only. Body: `commit` (40 hex), optional `name` and `body`, and `assets` as `{job, path, name}` already used by `opPublishRelease`. Do not call `gitrepo.Tag` and do not accept `replace`. Return 404 when the git tag is absent. Return 409 when the tag points at a commit other than `commit`. Require a running job in this repository whose pipeline ref is `refs/tags/{tag}` and whose `CI_COMMIT_SHA` equals `commit`. Resolve each asset with `Store.ArtifactPath` from that same pipeline and copy it under `releases/{owner}/{repo}/{tag}/{name}`. If no release row exists, insert one for the existing tag. If a release row exists from a job on this same tag, replace only those named asset files after the new copies succeed, and leave the git tag and every other release alone. If the release was made by hand or from another ref, return 409 and change nothing. Reject asset names that contain `/` or `..`. Until that route exists, this project's tag job cannot publish.
236
237## 16. Synthetic test bodies
238
239`tests/fixtures/bodies/test-body-male.glb` and `test-body-female.glb` are generated by `scripts/generate_test_bodies.py`. No external model is downloaded. Both files are self-contained glTF 2.0 binary, CC0-1.0, and labelled in `asset.extras` as test assets with `finalCharacter: false`. They are not final characters.
240
241Each file has one skin, 19 named joints, rigid skin weights, and two clips: `walk` (1 second, arms opposite the legs) and `attack` (0.8 seconds, the right arm swings forward). Bind rotations are identity. The documented axes are +Y up and forward -Z. Male and female differ in shoulder width, hip width and hip height so a later importer can tell them apart.
242
243`tests/test_body_assets.rs` checks that both files exist, parse as GLB, contain that skeleton, contain both clips, and byte-match a fresh run of the generator. That test is a fixture check. It is not the product importer, it does not play the clips, and it does not render. The desktop shell still reports glTF/GLB as not built and does not open these files.
244
245# Appendix A — Tim's original proposal (verbatim)
246
247The following block preserves the original Dutch text including spelling and filename placeholders. It is not a paraphrase or implementation status.
248
249```text
250Ik heb een idee voor een projectje;
251Een Paperdoll Renderer.
252
253Het idee;
254We importeren twee menselijke 3D-modellen; een man en een vrouw. Die hebben een set animaties, zoals lopen, springen, bukken, aanvallen, blokkeren, geraakt worden, etc. (we lezen gewoon alles uit het model)
255
256Bij renderen hebben we twee modussen: Karaktermodel, en uitrusting (hier later meer over). Als je rendert, dan hebben we 3*12 posities; 3 pitches (-30 graden, 0 graden, +30 graden), en 12 yaw (beginnend met recht naar de camera kijken en dan steeds een stapje ronddraaien tot we alle hoeken hebben). We renderen die naar PNG's met de naam: <geslacht>-<settype>-<animatienaam>-<pitch>-<yaw>-<maptype>-f<framenummer>.png met een vooringestelde (hoge, acht keer de doelresolutie) resolutie en framerate.
257We pakken dan die hoge resolutie, en maken een detail-map; dat is effectief een edge-detection algoritme. We schalen de paperdoll dan omlaag naar de doelresolutie, en we gebruiken de detail-map om pixels extra gewicht te geven om te kiezen welke kleur we een pixel geven.
258Daarna maken we een depth-map op de doelresolutie, en een normal map. (de maptypes in de filename zijn dus: diffuse, depth, normal)
259Als we de normalmap en depthmap hebben, doen we nog een pass over de diffuse om randen een beetje te highlighten. Daarna slaan we de hele set op volgens het afgesproken naamformat (allemaal op de doelresolutie). Dit is is settype=body.
260
261Daarna hebben we ook equipment. Je kiest een geslacht, en je moet per lichaamsdeel (op de skeleton van het karaktermodel) een 3D-model kunnen importeren, en op de bone kunnen positioneren. Als je alle modellen van een stuk equipment op de juiste positie gezet hebt, kun je dat opslaan als een preset (binary file met de modellen en posities op bones, alsmede een flag voor of het male of female is). Je kunt hem ook renderen; dan kies je een naam voor je equipment. Die naam is de settype; allemaal lowercase, geen spaties toegestaan. Dit rendert dan nogmaals alles, net als bij de body, alleen is de body dan doorzichtig, waardoor je alleen de equipment-modellen tekent. Equipment heeft ook een shinymap; dat is hoeveel specular een pixel moet hebben voor later bij de renderer.
262
263De bedoeling is dat we die paperdolls later kunnen gebruiken om een renderer te schrijven. Die gebruikt de diffuse maps en laagt met equipments over elkaar; het gebruikt de depth-map om te bepalen welke pixel van welke map (body of equipment) getekent moet worden (soort z-check), en de normal map om bumpmapping en lighting te tekenen. Dat wordt dan deel van een character-renderer en controller.
264
265Als stap 1 wil ik graag de paperdoll renderer maken. Ik heb nog geen modellen, die moet ik dus later zelf in het programma kunnen selecteren. Voor positioneren op bones zijn XYZ voor relatieve positie en rotatie voldoende voor nu, met dan een live preview waarop je het lichaam ziet, die je kunt roteren (3D model view, niet de gerenderde plaatjes want die zijn er dan nog niet).
266
267Wanneer je sets maakt, is dat in een .ppd (PaPerDoll) project; een map met een .ppd file die verteld welke modellen gebruikt worden, welke binary files van equipment sets er zijn, en waar gerenderde output staat.
268Zo kunnen we ook een preview tab maken; kies een geslacht, kies equipments om te layeren, en kies een animatie. Je ziet dan de animatie op loop spelen (de gerenderde 2D paperdolls), en kunt met pijltjes orbiten (boven, midden, onder, en dan voor links en rechts de 12 rotaties). Ook moet je met knopjes die je ingedrukt kan houden de positie van de lichtbron kunnen roteren. Dat gaat in graden, niet in stappen.
269
270Stappenplan:
271
272- Maak een groep op BlueGit: `Golden Age`
273- Maak de repo, Paperdoll Renderer
274
275- Documenteer alle bovenstaande, incl. letterlijke tekst van mij, zeer gedetailleerd in een enkele design.md
276- Bouw dan alle features een voor een in Rust. Elke feature op een nieuwe branch, en als die feature af is, niet-FF mergen naar master (Geen MR's tijdens eerste ontwikkelfase) en door naar de volgende feature. Elke feature krijgt een unit test die we in de CI-CD activeren op een merge request (ook al gebruiken we die nog niet) en op een master push. Een tag push krijgt, als tests slagen, ook een build; AppImage, Ubuntu Package, Arch Package, Windows Build, welke we dan als een release pushen.
277
278Houd me bij elke feature op de hoogte hoe het gaat.
279```
280
281# Appendix B — Additional import requirement (verbatim)
282
283```text
284Bepaal ook welk 3D-model format goed is; liefst ondersteunen we natuurlijk meerdere. In ieder geval .obj (met mtl en texture) en .fbx, maar hoe meer hoe beter.
285```