Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .tickets/cwb-ddf7.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
id: cwb-ddf7
status: open
status: in_progress
deps: []
links: []
type: feature
Expand All @@ -20,6 +20,14 @@ The web bridge and viewer own deterministic inspection payloads and
presentation; they do not own example geometry, kernel semantics, or a generic
plugin system.

## Addressable session extension

Retain the `panel_trio` assembly behind a Wasm `InspectionSession`, then let a
picked face identify its exact instance address and constructive material slot.
The bridge owns this session and its JSON presentation. Exedra continues to own
assembly state, material policy, and typed addressed operations; this work does
not extract a generic tooling schema.

## Acceptance

- Dependency direction is documented before implementation.
Expand All @@ -28,3 +36,5 @@ plugin system.
- Group order, instance identity, and coordinate convention remain stable.
- Missing provenance is displayed as missing.
- Existing scenarios remain byte-deterministic.
- Repeated session snapshots observe one explicit runtime space and revision.
- A picked material-bearing face yields an instance-address/material-slot pair.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/cambium_web_bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ categories = ["graphics", "wasm"]
crate-type = ["cdylib", "rlib"]

[dependencies]
addressable.workspace = true
cambium = { features = ["libm"], workspace = true }
exedra = { features = ["libm"], workspace = true }
exedra_assembly.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions apps/cambium_web_bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ Inspection scenarios (`list_inspection_scenarios_json()`):
- `panel_trio`: the multi-instance assembly scene; three placements share
one provenance-attributed body.

`InspectionSession::new("panel_trio", space)` retains that assembly as one
Addressable runtime across calls. Its `snapshot_json()` method projects the
current host through the same `cambium-inspect-v1` payload, while `space` and
`revision` expose the observation context. The one-shot function remains
available for recipe-only scenarios and compatibility.

Current scenarios:
- `stepped_tower`
- `pedestal`
Expand Down
111 changes: 107 additions & 4 deletions apps/cambium_web_bridge/src/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use addressable::SpaceId;
use exedra_constructive::evaluate::{Evaluation, Fidelity, Severity, evaluate};
use exedra_constructive::ir::{
CapMode, CsgOp, NodeId, NodeKind, Placement3, Recipe, RecipeBuilder,
Expand All @@ -36,6 +37,80 @@ use wasm_bindgen::prelude::*;

use crate::{MeshBuffers, extract_mesh_buffers, matrix16, panel_trio_assembly};

/// A live Addressable assembly behind the Cambium provenance inspector.
///
/// The first stateful scenario is `panel_trio`. Constructing a session assigns
/// that assembly a caller-supplied runtime space identity; repeated snapshots
/// then observe the same host and revision instead of rebuilding an unrelated
/// assembly for every bridge call.
///
/// Recipe-only inspection scenarios remain available through
/// [`run_inspection_scenario_json`].
#[wasm_bindgen]
#[derive(Debug)]
pub struct InspectionSession {
space: exedra_assembly::AddressableAssembly,
}

#[wasm_bindgen]
impl InspectionSession {
/// Starts a live inspection session for one named assembly scenario.
///
/// # Errors
///
/// Returns a JavaScript error when `name` is not a stateful assembly
/// scenario or its assembly cannot be constructed.
#[wasm_bindgen(constructor)]
pub fn new(name: &str, space: u64) -> Result<Self, JsValue> {
Self::build(name, space).map_err(|error| JsValue::from_str(&error))
}

/// Returns the caller-assigned runtime Addressable space identity.
#[wasm_bindgen(getter)]
#[must_use]
pub fn space(&self) -> u64 {
self.space.id().get()
}

/// Returns the current space-local Addressable revision.
#[wasm_bindgen(getter)]
#[must_use]
pub fn revision(&self) -> u64 {
self.space.revision().get()
}

/// Projects the current assembly as a `cambium-inspect-v1` snapshot.
///
/// # Errors
///
/// Returns a JavaScript error when compilation, evaluation, or
/// serialization fails.
pub fn snapshot_json(&self) -> Result<String, JsValue> {
let response = self.snapshot().map_err(|error| JsValue::from_str(&error))?;
serde_json::to_string(&response).map_err(|error| {
JsValue::from_str(&format!("failed to serialize inspection response: {error}"))
})
}
}

impl InspectionSession {
fn build(name: &str, space: u64) -> Result<Self, String> {
if name != "panel_trio" {
return Err(format!(
"inspection scenario `{name}` does not own an Addressable assembly"
));
}
let assembly = panel_trio_assembly()?;
Ok(Self {
space: assembly.into_addressable(SpaceId::new(space)),
})
}

fn snapshot(&self) -> Result<InspectionResponse, String> {
inspect_panel_trio_assembly(self.space.assembly())
}
}

/// The inspection payload format identifier.
pub const INSPECT_FORMAT: &str = "cambium-inspect-v1";

Expand Down Expand Up @@ -333,14 +408,20 @@ fn inspect_recipe(name: &str, recipe: &Recipe) -> Result<InspectionResponse, Str
/// the part recipe (full provenance), instances from the assembly's
/// flattened render list.
fn inspect_panel_trio() -> Result<InspectionResponse, String> {
let assembly = panel_trio_assembly()?;
inspect_panel_trio_assembly(&assembly)
}

fn inspect_panel_trio_assembly(
assembly: &exedra_assembly::Assembly,
) -> Result<InspectionResponse, String> {
use exedra_assembly::{PartCompiler, PartSource, flatten};

let asm = panel_trio_assembly()?;
let mut compiler = PartCompiler::new();
let compiled = compiler
.compile_parts(&asm, &EvalPolicy::default())
.compile_parts(assembly, &EvalPolicy::default())
.map_err(|e| format!("{e}"))?;
let list = flatten(&asm, &compiled);
let list = flatten(assembly, &compiled);

let mut response = empty_response("panel_trio");
// Evaluate each distinct part's recipe once for provenance; the body
Expand All @@ -354,7 +435,7 @@ fn inspect_panel_trio() -> Result<InspectionResponse, String> {
let body = match body_lookup.get(&key).copied() {
Some(body) => body,
None => {
let def = asm
let def = assembly
.part(item.part)
.ok_or_else(|| "flatten referenced an unknown part".to_string())?;
let part_key = def.key().to_string();
Expand Down Expand Up @@ -730,4 +811,26 @@ mod tests {
assert_eq!(json(name), json(name), "{name}: byte-identical reruns");
}
}

#[test]
fn panel_trio_session_retains_space_revision_and_snapshot_identity() {
let session = InspectionSession::build("panel_trio", 41).expect("session starts");
assert_eq!(session.space(), 41);
assert_eq!(session.revision(), 0);

let first = serde_json::to_string(&session.snapshot().expect("first snapshot"))
.expect("first snapshot serializes");
let second = serde_json::to_string(&session.snapshot().expect("second snapshot"))
.expect("second snapshot serializes");
assert_eq!(first, second);
assert_eq!(session.space(), 41);
assert_eq!(session.revision(), 0);
}

#[test]
fn session_rejects_recipe_only_scenarios() {
let error = InspectionSession::build("drilled_block", 41)
.expect_err("recipe scenario has no live assembly");
assert!(error.contains("does not own an Addressable assembly"));
}
}
4 changes: 4 additions & 0 deletions apps/cambium_web_viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ a summary readout and the exact diagnostics ledger. The selected face
highlights in the accent color. Reloading a scenario re-renders the
byte-identical payload.

The `panel_trio` inspector retains an `InspectionSession` backed by one
Addressable assembly. Recipe-only scenarios continue to use immutable
snapshots; reloading `panel_trio` intentionally starts a new runtime space.

## Viewer Controls

- Scenario picker: choose and rerun a named flow.
Expand Down
24 changes: 21 additions & 3 deletions apps/cambium_web_viewer/src/inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ type InspectionResponse = {
type WasmApi = {
list_inspection_scenarios_json: () => string;
run_inspection_scenario_json: (name: string) => string;
InspectionSession: new (name: string, space: bigint) => WasmInspectionSession;
};

type WasmInspectionSession = {
readonly space: bigint;
readonly revision: bigint;
snapshot_json: () => string;
free: () => void;
};

// --- DOM ---
Expand Down Expand Up @@ -549,6 +557,8 @@ async function loadWasmApi(): Promise<WasmApi> {

async function bootstrap(): Promise<void> {
const wasm = await loadWasmApi();
let currentSession: WasmInspectionSession | null = null;
let nextSpace = 1n;
const scenarios = JSON.parse(wasm.list_inspection_scenarios_json()) as string[];
for (const name of scenarios) {
const option = document.createElement("option");
Expand All @@ -560,9 +570,17 @@ async function bootstrap(): Promise<void> {
const runScenario = (): void => {
const selectedName = scenarioSelect.value;
try {
const response = JSON.parse(
wasm.run_inspection_scenario_json(selectedName),
) as InspectionResponse;
currentSession?.free();
currentSession = null;
let responseJson: string;
if (selectedName === "panel_trio") {
currentSession = new wasm.InspectionSession(selectedName, nextSpace);
nextSpace += 1n;
responseJson = currentSession.snapshot_json();
} else {
responseJson = wasm.run_inspection_scenario_json(selectedName);
}
const response = JSON.parse(responseJson) as InspectionResponse;
currentResponse = response;
formatLabel.textContent = response.format;
buildScene(response);
Expand Down
7 changes: 7 additions & 0 deletions apps/cambium_web_viewer/src/wasm_pkg.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@ declare module "./wasm_pkg/cambium_web_bridge" {
export function run_scenario_json(name: string, optionsJson: string): string;
export function list_inspection_scenarios_json(): string;
export function run_inspection_scenario_json(name: string): string;
export class InspectionSession {
constructor(name: string, space: bigint);
readonly space: bigint;
readonly revision: bigint;
snapshot_json(): string;
free(): void;
}
}
Loading