Reach the street-object placeholder spawn path headlessly with --placeholder kind=path #310

Closed
opened 2026-09-30 04:01:38 +00:00 by jeroen · 0 comments
Owner

Problem

The placeholder feature lets an operator replace built-in street objects (lamppost, bench, parked car and so on) with a model from the library. The step that places those models has never run. docs/notes/street-objects-can-wear-a-model.md (section "Unverified") says: "the geometry half — build_furniture_meshes_except, the mask, the queue — is unit-tested, and the spawn path is not". It also names the next step: "a --placeholder kind=<file.glb> flag reading a local file through the ordinary AssetServer".

Two things stop a scripted capture from running that step today:

  1. The table only ever comes from a server. crates/cartopolis/src/systems/map/placeholder_models.rs:160 poll_placeholders fetches /api/library/placeholders, and when model_assets::have_endpoint() is false it clears the table (models.adopt(Vec::new()), around line 186). Every scripted run is offline, so a seeded table would be wiped the next frame.
  2. spawn_placeholder_models (line 249) always loads model_assets::asset_path(&hash), i.e. sim://{hash}.glb, which needs a server session.

The harness also cannot see the spawn queue. No ShotMetrics field counts queued or spawned placeholder instances, so a capture can report settled=true with the queue still draining at MAX_SPAWNS_PER_FRAME (64 per frame).

Evidence

  • docs/notes/street-objects-can-wear-a-model.md, section "Unverified" (quoted above).
  • placeholder_models.rs:160-205: the fetch and the offline clear. :249-300: the spawn, which always uses asset_path(hash).
  • map_geometry.rs:2582-2612: the enqueue step, which is fed only by stores.placeholders for the forms in its mask. map_geometry.rs:2363: the mask goes into DetailWants.modelled. map_geometry.rs:1756: build_furniture_meshes_except skips those forms when meshing.
  • grep finds no placeholder field in crates/cartopolis/src/systems/dev/shot_harness.rs.

Approach

  • Placeholder gets a source that is either a library hash (as today) or a local asset path. spawn_placeholder_models loads GltfAssetLabel::Scene(0).from_asset(...) from whichever it holds.
  • Add a CLI flag --placeholder KIND=PATH[,scale=S][,yaw=DEG], repeatable. KIND is checked with FurnitureKind::from_slug, and an unknown slug is a startup error (same rule as unknown manifest and --expect keys). The flag seeds PlaceholderModels at startup and marks it local, so poll_placeholders neither fetches nor clears it. Scripted runs only: the table is world-wide by design, and a user-facing local override would contradict the module's first property.
  • Tag each spawned instance with a marker component. Add two fields to ShotMetrics: placeholder_spawned (live instances) and placeholder_queued (queue length). Put placeholder_queued > 0 in the harness's busy list so a capture does not settle mid-drain.
  • For the fixture, use an existing self-contained glTF under assets/models/avatar/characters/ (e.g. Man_Casual.gltf), so no new binary goes into LFS.

Acceptance criteria

  • A unit test: parsing --placeholder bench=models/x.gltf,scale=0.5,yaw=90 gives the right kind, path, scale and angle in radians. An unknown kind is an error.
  • A unit test: with a local table seeded, poll_placeholders with no endpoint leaves the table and its mask alone.
  • placeholder_spawned and placeholder_queued appear in --dump-state and --csv, and asserting them with --expect is accepted.
  • A headless shot over a Groningen street, run with --placeholder for a form the default vector tiles place there (bench or waste basket), exits 0 with --expect 'settled==true' --expect 'placeholder_spawned>0' --expect 'placeholder_queued==0'.
  • The same viewpoint without the flag reports placeholder_spawned==0.
  • The "Unverified" section of docs/notes/street-objects-can-wear-a-model.md is updated with the measured count and the command that produced it.
  • cargo test --locked --workspace --exclude cartopolis_android passes.

Verification

cd "$(slot take)"
slot run -- cargo test -p cartopolis placeholder
slot run -- bash -lc 'WGPU_BACKEND=vulkan cargo run -p cartopolis -- --shot /tmp/ph.png --at 53.2194,6.5665,60 --look=-30,0 --hide-ui --placeholder bench=models/avatar/characters/Man_Casual.gltf --dump-state /tmp/ph.json --expect settled==true --expect "placeholder_spawned>0" --expect placeholder_queued==0'

This only checks geometry and counts, so the software rasteriser on the container is enough.

Out of scope

  • Any change to the server's placeholder table, /api/library/placeholders, the wire or the schema.
  • A user-facing local override outside scripted runs.
  • Surveyed street objects drawn by other streamers (e.g. street_detail) that do not go through map_geometry's enqueue step.
  • Judging how the model looks.

Direction

Serves value 6 (the feature becomes checkable by counts in --dump-state and --expect gates instead of by looking) and value 7 (the note's "Unverified" section is replaced with a measured count). It touches no wire protocol, no database schema, nothing that has to be looked at to judge, no AR, no Android publishing and no host.


Filed unattended by the steward from e4fae5dbe6. To reject, close it with a sentence saying why — the next run reads that.

## Problem The placeholder feature lets an operator replace built-in street objects (lamppost, bench, parked car and so on) with a model from the library. The step that places those models has never run. `docs/notes/street-objects-can-wear-a-model.md` (section "Unverified") says: "the geometry half — `build_furniture_meshes_except`, the mask, the queue — is unit-tested, and the spawn path is not". It also names the next step: "a `--placeholder kind=<file.glb>` flag reading a local file through the ordinary `AssetServer`". Two things stop a scripted capture from running that step today: 1. The table only ever comes from a server. `crates/cartopolis/src/systems/map/placeholder_models.rs:160` `poll_placeholders` fetches `/api/library/placeholders`, and when `model_assets::have_endpoint()` is false it clears the table (`models.adopt(Vec::new())`, around line 186). Every scripted run is offline, so a seeded table would be wiped the next frame. 2. `spawn_placeholder_models` (line 249) always loads `model_assets::asset_path(&hash)`, i.e. `sim://{hash}.glb`, which needs a server session. The harness also cannot see the spawn queue. No `ShotMetrics` field counts queued or spawned placeholder instances, so a capture can report `settled=true` with the queue still draining at `MAX_SPAWNS_PER_FRAME` (64 per frame). ## Evidence - `docs/notes/street-objects-can-wear-a-model.md`, section "Unverified" (quoted above). - `placeholder_models.rs:160-205`: the fetch and the offline clear. `:249-300`: the spawn, which always uses `asset_path(hash)`. - `map_geometry.rs:2582-2612`: the enqueue step, which is fed only by `stores.placeholders` for the forms in its mask. `map_geometry.rs:2363`: the mask goes into `DetailWants.modelled`. `map_geometry.rs:1756`: `build_furniture_meshes_except` skips those forms when meshing. - `grep` finds no `placeholder` field in `crates/cartopolis/src/systems/dev/shot_harness.rs`. ## Approach - `Placeholder` gets a source that is either a library hash (as today) or a local asset path. `spawn_placeholder_models` loads `GltfAssetLabel::Scene(0).from_asset(...)` from whichever it holds. - Add a CLI flag `--placeholder KIND=PATH[,scale=S][,yaw=DEG]`, repeatable. `KIND` is checked with `FurnitureKind::from_slug`, and an unknown slug is a startup error (same rule as unknown manifest and `--expect` keys). The flag seeds `PlaceholderModels` at startup and marks it local, so `poll_placeholders` neither fetches nor clears it. Scripted runs only: the table is world-wide by design, and a user-facing local override would contradict the module's first property. - Tag each spawned instance with a marker component. Add two fields to `ShotMetrics`: `placeholder_spawned` (live instances) and `placeholder_queued` (queue length). Put `placeholder_queued > 0` in the harness's busy list so a capture does not settle mid-drain. - For the fixture, use an existing self-contained glTF under `assets/models/avatar/characters/` (e.g. `Man_Casual.gltf`), so no new binary goes into LFS. ## Acceptance criteria - [ ] A unit test: parsing `--placeholder bench=models/x.gltf,scale=0.5,yaw=90` gives the right kind, path, scale and angle in radians. An unknown kind is an error. - [ ] A unit test: with a local table seeded, `poll_placeholders` with no endpoint leaves the table and its mask alone. - [ ] `placeholder_spawned` and `placeholder_queued` appear in `--dump-state` and `--csv`, and asserting them with `--expect` is accepted. - [ ] A headless shot over a Groningen street, run with `--placeholder` for a form the default vector tiles place there (bench or waste basket), exits 0 with `--expect 'settled==true' --expect 'placeholder_spawned>0' --expect 'placeholder_queued==0'`. - [ ] The same viewpoint without the flag reports `placeholder_spawned==0`. - [ ] The "Unverified" section of `docs/notes/street-objects-can-wear-a-model.md` is updated with the measured count and the command that produced it. - [ ] `cargo test --locked --workspace --exclude cartopolis_android` passes. ## Verification ```bash cd "$(slot take)" slot run -- cargo test -p cartopolis placeholder slot run -- bash -lc 'WGPU_BACKEND=vulkan cargo run -p cartopolis -- --shot /tmp/ph.png --at 53.2194,6.5665,60 --look=-30,0 --hide-ui --placeholder bench=models/avatar/characters/Man_Casual.gltf --dump-state /tmp/ph.json --expect settled==true --expect "placeholder_spawned>0" --expect placeholder_queued==0' ``` This only checks geometry and counts, so the software rasteriser on the container is enough. ## Out of scope - Any change to the server's placeholder table, `/api/library/placeholders`, the wire or the schema. - A user-facing local override outside scripted runs. - Surveyed street objects drawn by other streamers (e.g. `street_detail`) that do not go through `map_geometry`'s enqueue step. - Judging how the model looks. ## Direction Serves value 6 (the feature becomes checkable by counts in `--dump-state` and `--expect` gates instead of by looking) and value 7 (the note's "Unverified" section is replaced with a measured count). It touches no wire protocol, no database schema, nothing that has to be looked at to judge, no AR, no Android publishing and no host. --- _Filed unattended by the steward from `e4fae5dbe6`. To reject, close it with a sentence saying why — the next run reads that._
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#310
No description provided.