Add project listing selector, confirmed revision and private 3MF renders

This commit is contained in:
2026-11-08 18:49:32 +00:00
parent e62b6800a6
commit 4e675510fd
29 changed files with 3394 additions and 14 deletions
+5
View File
@@ -71,6 +71,9 @@ Projects are the product aggregate: a reversible internal lifecycle (Design, Rea
- Releasing a revision to a listing mapping requires a cost snapshot for that same revision with every category costed (filament, packaging and shipping — even as an explicit `0`, but not missing) and pins an exact revision + cost snapshot; uploading a newer revision never moves that pointer on its own, and both release and revert create a new append-only `project_releases` row under an optimistic lock (`project_listing_links.lock_version`) rather than overwriting history. A detached listing mapping can no longer be released or reverted to.
- Cost snapshots use fixed-point integer arithmetic (`App\Support\FixedPoint` / `App\Services\ProjectCosting`), not floats: the bcmath/gmp PHP extensions are not installed in this project's runtime, so exact-decimal math is done by scaling decimal strings into bounded native integers with explicit overflow and storable-precision checks. Estimate and actual snapshots are both append-only and never overwrite each other; a missing cost category is stored as `null` ("not costed"), distinct from an intentional `0.0000`. Each category accepts exactly one quantity unit (filament `g`, packaging `unit`, shipping `parcel`) so a mistyped unit is rejected rather than silently mis-costed. Snapshot currency must match the project (no implied FX). Shipping quantities are parcels; `units_per_parcel` allocates that parcel cost across the saleable batch yield (`batch_units / units_per_parcel * parcel cost`) independently of `perUnit()`. Actual shipping needs a confirmed shipment or invoice reference. The form starts with separate filament, packaging and shipping lines and can add more materials/components; Fusion project/document/version fields on a revision are optional manual provenance only.
- Stage can only move to Live once *every* non-detached listing mapping has an active release (not merely one of several); moving a project's internal stage never calls the Etsy API or publishes anything. Attaching a new, not-yet-released listing mapping to a project already at Live drops its stage back to Design rather than leaving Live claiming coverage it no longer has.
- Linking an Etsy listing uses a dropdown of the shop's complete Live and Draft listings together (optgroups, title + ID + state), not a free-typed ID — fetched in full via the existing cached Etsy service, bounded to 2000 listings per state (20 pages of 100), and failing explicitly rather than silently showing a truncated list if a shop exceeds that or a page comes back short. The server always re-verifies the submitted ID's shop ownership and Live/Draft state on its own, independent of what the dropdown offered.
- A project has one "confirmed current revision" pointer (`projects.current_revision_id`), separate from each listing mapping's own `liveRelease` pointer — it marks the project's current source of truth independent of what is released to any listing. Only an explicit confirm or clear action moves it (never an upload), under an optimistic lock (`projects.lock_version`); rolling back to an older approved revision is just re-confirming it, never mutating revision history.
- "Generate product image" (per uploaded `.3mf` file, admin-only, explicit POST, CSRF-protected) renders a real numeric preview from that file's own mesh — never a thumbnail or a stock photo. It parses the 3MF scene graph itself (objects, nested components, build-item instances, `<basematerials>`/`<colorgroup>` colours) as its own bounded, XXE-safe security boundary, then does a real isometric projection with backface culling and per-face Lambertian shading, rasterised to PNG via GD with a genuine per-pixel depth buffer (or a fixed-content, numeric-only SVG if GD is unavailable). Unsupported input — transparent materials, texture resources, external/multi-part model references, a pid that doesn't resolve to a known colour group, an out-of-range colour index, or a triangle with genuinely interpolated per-vertex colours — is rejected with a clear error rather than silently rendered wrong. The render is cached per source file on the private `project_renders` disk (not web-served); regenerating an existing render is a no-op, concurrent requests are serialised by a project-row lock, generation never happens on a page view, and it never modifies the original upload or uploads anything to Etsy.
## Branding
@@ -85,6 +88,8 @@ A restorable backup needs **both** of the following, taken together (the private
Verify a restore against an isolated copy before trusting it, and spot-check a `project_files.sha256` value against the restored bytes on disk.
Generated product images at `storage/app/project-renders/` must also be included alongside their `project_file_renders` database rows. Although derived from source geometry, existing records are idempotent and do not automatically rebuild missing bytes; restoring only the database leaves broken previews. Keep the database, source files and generated-image disk together.
## Verification
```sh