From eaaa7407337471e5c67ab48adc720136a3582299 Mon Sep 17 00:00:00 2001 From: Christian Johannesen Date: Wed, 28 Oct 2026 10:31:27 +0100 Subject: [PATCH] Implement M2 deterministic botanical SVG renderer and visual studies --- README.md | 16 +- apps/server/package.json | 3 +- apps/server/src/app.ts | 24 +- apps/server/src/preview-route.test.ts | 41 ++ apps/server/src/preview-route.ts | 35 ++ apps/web/package.json | 5 +- apps/web/src/components/PlantPreview.tsx | 43 ++ apps/web/src/lib/exportPlant.ts | 61 +++ apps/web/src/lib/studies.ts | 65 +++ apps/web/src/lib/studyStore.ts | 18 + apps/web/src/main.tsx | 14 +- apps/web/src/pages/RenderGallery.tsx | 83 ++++ apps/web/src/pages/RenderLab.tsx | 235 +++++++++++ apps/web/src/styles.css | 397 ++++++++++++++++++ artifacts/m2/atlas-desktop.png | Bin 0 -> 506619 bytes artifacts/m2/atlas-mobile.png | Bin 0 -> 191638 bytes artifacts/m2/browser-checks.json | 73 ++++ artifacts/m2/export-transparent.png | Bin 0 -> 89239 bytes artifacts/m2/export-transparent.svg | 1 + artifacts/m2/export-warm.png | Bin 0 -> 87497 bytes artifacts/m2/export-warm.svg | 1 + artifacts/m2/hero-512.png | Bin 0 -> 63234 bytes artifacts/m2/icon-64.png | Bin 0 -> 3408 bytes artifacts/m2/lab-desktop.png | Bin 0 -> 169708 bytes artifacts/m2/lab-mobile.png | Bin 0 -> 124133 bytes artifacts/m2/smoke-desktop.png | Bin 0 -> 176620 bytes assets/README.md | 11 +- assets/species/velvetleaf/bloom_cup_v1.svg | 1 + assets/species/velvetleaf/bloom_double_v1.svg | 1 + assets/species/velvetleaf/bloom_round_v1.svg | 1 + assets/species/velvetleaf/bloom_star_v1.svg | 1 + assets/species/velvetleaf/branch_bushy_v1.svg | 1 + .../species/velvetleaf/branch_compact_v1.svg | 1 + .../species/velvetleaf/branch_trailing_v1.svg | 1 + assets/species/velvetleaf/flower_stalk_v1.svg | 1 + assets/species/velvetleaf/leaf_lobed_v1.svg | 1 + assets/species/velvetleaf/leaf_oval_v1.svg | 1 + assets/species/velvetleaf/leaf_pointed_v1.svg | 1 + assets/species/velvetleaf/manifest.json | 283 +++++++++++++ assets/species/velvetleaf/pot_ceramic_v1.svg | 1 + .../species/velvetleaf/pot_terracotta_v1.svg | 1 + assets/species/velvetleaf/shadow_v1.svg | 1 + assets/species/velvetleaf/stem_v1.svg | 1 + docs/API_AND_DATA.md | 2 + docs/DEPLOYMENT.md | 3 +- docs/GRAPHICS_SPEC.md | 10 + docs/PROGRESS.md | 25 +- package-lock.json | 10 +- package.json | 5 +- packages/renderer/package.json | 4 + packages/renderer/src/index.ts | 6 +- packages/renderer/src/layout.ts | 174 ++++++++ packages/renderer/src/palettes.ts | 66 +++ packages/renderer/src/primitives.ts | 151 +++++++ packages/renderer/src/renderSvg.ts | 172 ++++++++ packages/renderer/src/renderer.test.ts | 155 +++++++ packages/renderer/src/types.ts | 51 +++ scripts/verify-m2.cjs | 216 ++++++++++ scripts/write-assets.mjs | 76 ++++ 59 files changed, 2523 insertions(+), 27 deletions(-) create mode 100644 apps/server/src/preview-route.test.ts create mode 100644 apps/server/src/preview-route.ts create mode 100644 apps/web/src/components/PlantPreview.tsx create mode 100644 apps/web/src/lib/exportPlant.ts create mode 100644 apps/web/src/lib/studies.ts create mode 100644 apps/web/src/lib/studyStore.ts create mode 100644 apps/web/src/pages/RenderGallery.tsx create mode 100644 apps/web/src/pages/RenderLab.tsx create mode 100644 artifacts/m2/atlas-desktop.png create mode 100644 artifacts/m2/atlas-mobile.png create mode 100644 artifacts/m2/browser-checks.json create mode 100644 artifacts/m2/export-transparent.png create mode 100644 artifacts/m2/export-transparent.svg create mode 100644 artifacts/m2/export-warm.png create mode 100644 artifacts/m2/export-warm.svg create mode 100644 artifacts/m2/hero-512.png create mode 100644 artifacts/m2/icon-64.png create mode 100644 artifacts/m2/lab-desktop.png create mode 100644 artifacts/m2/lab-mobile.png create mode 100644 artifacts/m2/smoke-desktop.png create mode 100644 assets/species/velvetleaf/bloom_cup_v1.svg create mode 100644 assets/species/velvetleaf/bloom_double_v1.svg create mode 100644 assets/species/velvetleaf/bloom_round_v1.svg create mode 100644 assets/species/velvetleaf/bloom_star_v1.svg create mode 100644 assets/species/velvetleaf/branch_bushy_v1.svg create mode 100644 assets/species/velvetleaf/branch_compact_v1.svg create mode 100644 assets/species/velvetleaf/branch_trailing_v1.svg create mode 100644 assets/species/velvetleaf/flower_stalk_v1.svg create mode 100644 assets/species/velvetleaf/leaf_lobed_v1.svg create mode 100644 assets/species/velvetleaf/leaf_oval_v1.svg create mode 100644 assets/species/velvetleaf/leaf_pointed_v1.svg create mode 100644 assets/species/velvetleaf/manifest.json create mode 100644 assets/species/velvetleaf/pot_ceramic_v1.svg create mode 100644 assets/species/velvetleaf/pot_terracotta_v1.svg create mode 100644 assets/species/velvetleaf/shadow_v1.svg create mode 100644 assets/species/velvetleaf/stem_v1.svg create mode 100644 packages/renderer/src/layout.ts create mode 100644 packages/renderer/src/palettes.ts create mode 100644 packages/renderer/src/primitives.ts create mode 100644 packages/renderer/src/renderSvg.ts create mode 100644 packages/renderer/src/renderer.test.ts create mode 100644 packages/renderer/src/types.ts create mode 100644 scripts/verify-m2.cjs create mode 100644 scripts/write-assets.mjs diff --git a/README.md b/README.md index 4785ae8..3645abf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Plant Laboratory / Botanica -M0 provides a running React shell, Fastify API, PostgreSQL schema and repeatable bootstrap. Genetics and game workflows follow in M1–M4. +M0–M2 provide a running React laboratory, deterministic genetics, illustrated SVG renderer, Fastify API, PostgreSQL schema and repeatable bootstrap. Persistent plant creation and breeding follow in M3. ## Run @@ -34,8 +34,18 @@ docker compose run --rm bootstrap node dist/migrate.js docker compose run --rm bootstrap node dist/seed.js ``` -Migrations are checked-in SQL tracked in `schema_migrations`; seeds never overwrite existing records. `.env` is ignored by Git and Docker build contexts. Production containers run as non-root. The API returns safe uniform errors and health responds 503 if PostgreSQL is unavailable. Relative browser API URLs avoid container hostname leakage. `PUBLIC_BASE_URL` is validated and derived from origin plus port for localhost, or just origin behind a production reverse proxy. +Migrations are checked-in SQL tracked in `schema_migrations`; player seeds preserve existing records; the catalog seed synchronizes the current catalog definition without changing plants or ancestry. `.env` is ignored by Git and Docker build contexts. Production containers run as non-root. The API returns safe uniform errors and health responds 503 if PostgreSQL is unavailable. Relative browser API URLs avoid container hostname leakage. `PUBLIC_BASE_URL` is validated and derived from origin plus port for localhost, or just origin behind a production reverse proxy. -Routes: `/` redirects to `/lab`; `/lab`, `/breed`, `/gallery`, `/about` currently show clearly labeled milestone empty states. Original shell illustration is decorative; the deterministic species renderer is M2. No authentication: this sandbox is intended for local use. +Routes: `/` redirects to `/lab`, which contains curated rendering studies, four stages, cosmetic seed and manual golden-vein preview, derived trait inspection, and SVG/PNG export with warm or transparent background. `/gallery` is a proof atlas of 12 systematic variants plus a 100-seed rendering demo. `/breed` remains a clearly labeled M3 empty state; `/about` explains the fictional genetics model. These studies are not saved plants. No authentication: this sandbox is intended for local use. Read [the implementation plan](docs/IMPLEMENTATION_PLAN.md) and [progress log](docs/PROGRESS.md) for acceptance gates. + + +## Domain and artwork + +Genetics v1 uses fixed-locus diploid genomes, explicit dominance, seeded Mulberry32 inheritance, mutation logs and derived rarity. The catalog and phenotype preview APIs are available under `/api/v1`; see `docs/API_AND_DATA.md` for request examples. Pure packages use built JS exports; root build/typecheck/test/dev scripts first build package dependencies in order. + +Velvetleaf is composed from 15 original handcrafted SVG primitives and six curated palettes. `npm run assets:write` regenerates standalone primitive sources from the typed paths; tests reject source drift. `assets/README.md` documents CC0 artwork provenance. Geometry is deterministic and all exported assets are self-contained. + +For optional browser acceptance QA, install/provide Playwright and Chrome, then run `node scripts/verify-m2.cjs` against the running Compose app. `PLAYWRIGHT_MODULE` may point to an available Playwright package and `BROWSER_EXECUTABLE` to a browser binary. The script checks desktop/mobile overflow, 12/100 rendering studies, namespace collisions, seed invariance, real downloads, PNG alpha/background and pixel equivalence to the live preview. Screenshots and measurements are written to `artifacts/m2`. + diff --git a/apps/server/package.json b/apps/server/package.json index fb4dd30..133a613 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -16,7 +16,8 @@ "drizzle-orm": "^0.45.4", "pg": "^8.16.3", "@botanica/genetics": "0.0.0", - "@botanica/shared": "0.0.0" + "@botanica/shared": "0.0.0", + "@botanica/renderer": "0.0.0" }, "devDependencies": { "@types/pg": "^8.15.5", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index a8a6eb1..74bc811 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -1,3 +1,4 @@ +import { registerPreviewRoute } from "./preview-route.js"; import { ZodError } from "zod"; import { GeneticsError } from "@botanica/genetics"; import { registerGeneticsRoutes } from "./genetics-routes.js"; @@ -21,6 +22,7 @@ export function createApp( } }); registerGeneticsRoutes(app); + registerPreviewRoute(app); app.setNotFoundHandler((_request, reply) => reply .code(404) @@ -28,18 +30,16 @@ export function createApp( ); app.setErrorHandler((error, request, reply) => { if (error instanceof ZodError) - return reply - .code(400) - .send({ - error: { - code: "INVALID_REQUEST", - message: "Request validation failed.", - details: error.issues.map((i) => ({ - path: i.path, - message: i.message, - })), - }, - }); + return reply.code(400).send({ + error: { + code: "INVALID_REQUEST", + message: "Request validation failed.", + details: error.issues.map((i) => ({ + path: i.path, + message: i.message, + })), + }, + }); if (error instanceof GeneticsError) return reply .code(error.code === "INCOMPATIBLE_SPECIES" ? 409 : 400) diff --git a/apps/server/src/preview-route.test.ts b/apps/server/src/preview-route.test.ts new file mode 100644 index 0000000..0b34035 --- /dev/null +++ b/apps/server/src/preview-route.test.ts @@ -0,0 +1,41 @@ +import { it, expect } from "vitest"; +import { exampleGenome, resolvePhenotype } from "@botanica/genetics"; +import { layoutPlant, renderSvg } from "@botanica/renderer"; +import { createApp } from "./app.js"; +it("returns the same safe serialized SVG as the client renderer and rejects invalid preview inputs", async () => { + const app = createApp(async () => {}); + try { + const payload = { + speciesId: "velvetleaf", + genome: exampleGenome, + visualSeed: 91, + growthStage: "flowering", + background: "warm", + }; + const result = await app.inject({ + method: "POST", + url: "/api/v1/preview", + payload, + }); + expect(result.statusCode).toBe(200); + expect(result.json().svg).toBe( + renderSvg( + layoutPlant( + "velvetleaf", + resolvePhenotype("velvetleaf", exampleGenome), + 91, + "flowering", + ), + { namespace: "preview", background: "warm" }, + ), + ); + const bad = await app.inject({ + method: "POST", + url: "/api/v1/preview", + payload: { ...payload, visualSeed: "