brep_kernel/feature_pipeline/scene_metadata.rs
1//! Scene metadata — the kernel-owned name-keyed key/value store.
2//!
3//! The port of the retired `MetadataManager`: arbitrary metadata records against
4//! scene object NAMES (material assignments, layers, weld callouts, simulation
5//! attributes …) with single-parent inheritance via an `inheritsFrom` key —
6//! resolution merges the parent chain, child values winning. The caller side is
7//! now a thin proxy over these wasm exports; the store itself lives HERE and
8//! serializes with the part through [`scene_metadata_dump_json`] /
9//! [`scene_metadata_load_json`].
10//!
11//! TOPOLOGY metadata (per-face / per-edge: cap roles, sourceFeatureId, thread
12//! info, bend data) uses the same store keyed by the face/edge NAME — the
13//! kernel's persistent identity. Because names propagate through booleans on
14//! the records themselves, name-keyed metadata follows topology with zero
15//! propagation machinery. The pipeline stamps it (`stamp_face_metadata`);
16//! the caller's display layer reads it back per solid via
17//! `topo_metadata_for_names_json`.
18
19use std::cell::RefCell;
20use std::collections::HashMap;
21use wasm_bindgen::prelude::*;
22
23pub(crate) type Record_ = serde_json::Map<String, serde_json::Value>;
24
25thread_local! {
26 static SCENE_METADATA: RefCell<HashMap<String, Record_>> = RefCell::new(HashMap::new());
27}
28
29/// Merge the inheritance chain for `name` (child wins; cycle-guarded).
30fn resolve(store: &HashMap<String, Record_>, name: &str) -> Record_ {
31 let mut chain: Vec<&Record_> = Vec::new();
32 let mut seen: Vec<String> = Vec::new();
33 let mut current = name.to_string();
34 while let Some(record) = store.get(¤t) {
35 if seen.contains(¤t) {
36 break; // inheritance cycle — stop
37 }
38 seen.push(current.clone());
39 chain.push(record);
40 match record.get("inheritsFrom").and_then(|v| v.as_str()) {
41 Some(parent) if !parent.is_empty() => current = parent.to_string(),
42 _ => break,
43 }
44 }
45 let mut merged = Record_::new();
46 for record in chain.iter().rev() {
47 for (key, value) in record.iter() {
48 merged.insert(key.clone(), value.clone());
49 }
50 }
51 merged
52}
53
54/// Set (replace) the metadata record for a scene object name.
55#[wasm_bindgen]
56pub fn scene_metadata_set_json(name: &str, record_json: &str) -> Result<(), JsValue> {
57 let record: Record_ = serde_json::from_str(record_json)
58 .map_err(|error| JsValue::from_str(&format!("scene_metadata_set: {error}")))?;
59 SCENE_METADATA.with(|store| {
60 let mut store = store.borrow_mut();
61 if record.is_empty() {
62 store.remove(name);
63 } else {
64 store.insert(name.to_string(), record);
65 }
66 });
67 Ok(())
68}
69
70/// The RESOLVED metadata for a name (inheritance chain merged, child wins).
71#[wasm_bindgen]
72pub fn scene_metadata_get_json(name: &str) -> String {
73 SCENE_METADATA.with(|store| {
74 serde_json::Value::Object(resolve(&store.borrow(), name)).to_string()
75 })
76}
77
78/// The OWN metadata record only (no inheritance).
79#[wasm_bindgen]
80pub fn scene_metadata_get_own_json(name: &str) -> String {
81 SCENE_METADATA.with(|store| {
82 store
83 .borrow()
84 .get(name)
85 .map(|record| serde_json::Value::Object(record.clone()).to_string())
86 .unwrap_or_else(|| "{}".to_string())
87 })
88}
89
90/// Remove a whole record.
91#[wasm_bindgen]
92pub fn scene_metadata_remove(name: &str) {
93 SCENE_METADATA.with(|store| {
94 store.borrow_mut().remove(name);
95 });
96}
97
98/// The whole store (for part serialization).
99#[wasm_bindgen]
100pub fn scene_metadata_dump_json() -> String {
101 SCENE_METADATA.with(|store| {
102 serde_json::to_string(&*store.borrow()).unwrap_or_else(|_| "{}".to_string())
103 })
104}
105
106/// Replace the whole store (part load / reset — pass `{}` to clear).
107#[wasm_bindgen]
108pub fn scene_metadata_load_json(all_json: &str) -> Result<(), JsValue> {
109 let all: HashMap<String, Record_> = serde_json::from_str(all_json)
110 .map_err(|error| JsValue::from_str(&format!("scene_metadata_load: {error}")))?;
111 SCENE_METADATA.with(|store| {
112 *store.borrow_mut() = all;
113 });
114 Ok(())
115}
116
117/// Replace a record WHOLESALE (empty removes it) — the ACOMP stamping entry.
118/// A restored component's records are authoritative for its namespaced
119/// entities each run; merging instead would leave residue when a refreshed
120/// part's record DROPPED keys.
121pub(crate) fn set_record(name: &str, record: serde_json::Map<String, serde_json::Value>) {
122 SCENE_METADATA.with(|store| {
123 let mut store = store.borrow_mut();
124 if record.is_empty() {
125 store.remove(name);
126 } else {
127 store.insert(name.to_string(), record);
128 }
129 });
130}
131
132/// Take the WHOLE store out, leaving it empty — the isolated sub-part run
133/// bracket (`parts_library::rebuild_snapshot`): the sub-part's features stamp
134/// UN-namespaced records that must never merge into (or read) the parent
135/// document's records. Pair with [`restore_store`] on every exit path.
136pub(crate) fn take_store() -> HashMap<String, Record_> {
137 SCENE_METADATA.with(|store| std::mem::take(&mut *store.borrow_mut()))
138}
139
140/// Put a taken store back, discarding whatever the bracketed run stamped.
141pub(crate) fn restore_store(saved: HashMap<String, Record_>) {
142 SCENE_METADATA.with(|store| {
143 *store.borrow_mut() = saved;
144 });
145}
146
147/// The [`take_store`]/[`restore_store`] bracket as an RAII guard, for
148/// OUT-OF-CRATE producers that build a payload against a LIVE document.
149///
150/// The hazard it exists for: [`crate::snapshot_solids`] captures whatever record
151/// the thread's store holds for each name it is sealing. That is exactly right
152/// for a snapshot of the CURRENT scene (a face's material rides along), and
153/// exactly WRONG for a payload built out of freshly-named solids that were never
154/// in this scene — the STEP-assembly import lane's per-part
155/// [`crate::native_import_payload`] call being the first such producer. There,
156/// any collision between the names being stamped and names already in the
157/// document's store would silently seal the CURRENT document's metadata into the
158/// new part. Holding this guard across the encode makes the payload a pure
159/// function of its solids, which is what its determinism claim assumes.
160///
161/// Scope it to the ENCODE only. Anything that runs a history — the display
162/// rebuild, a `pump` — stamps records the document must keep, and dropping the
163/// guard afterwards would throw exactly those away.
164///
165/// Restores on drop, panics included; nesting is harmless (the inner guard takes
166/// an already-empty store and restores it).
167#[must_use = "the store is only isolated while the guard is alive"]
168pub struct IsolatedSceneMetadata {
169 saved: HashMap<String, Record_>,
170}
171
172impl IsolatedSceneMetadata {
173 /// Empty the thread's scene-metadata store until the guard drops.
174 pub fn begin() -> Self {
175 Self {
176 saved: take_store(),
177 }
178 }
179}
180
181impl Drop for IsolatedSceneMetadata {
182 fn drop(&mut self) {
183 restore_store(std::mem::take(&mut self.saved));
184 }
185}
186
187/// Merge keys into a record WITHOUT replacing it (existing keys win when
188/// `overwrite` is false) — the pipeline-side stamping entry.
189pub fn merge_record(name: &str, patch: &serde_json::Map<String, serde_json::Value>, overwrite: bool) {
190 if patch.is_empty() {
191 return;
192 }
193 SCENE_METADATA.with(|store| {
194 let mut store = store.borrow_mut();
195 let record = store.entry(name.to_string()).or_default();
196 for (key, value) in patch {
197 if overwrite || !record.contains_key(key) {
198 record.insert(key.clone(), value.clone());
199 }
200 }
201 });
202}
203
204/// Every record carrying an imported COLOUR, as `{name: "#RRGGBB"}`.
205///
206/// The narrow read the display layer needs. It exists because this store is
207/// THREAD-LOCAL to whichever thread ran the history: `BREP_render` executes a
208/// run on a background thread / worker, so anything the main thread wants must
209/// be read runner-side and shipped in the `RunOutput` (the same reason the
210/// assembly pose write-backs are read there). See `io/appearance.rs` for the
211/// record convention and `BREP_render/src/pipeline.rs` for the consumer.
212///
213/// Deliberately colour-only rather than a whole-store dump: every face carries
214/// `sourceFeatureId` and friends, and pushing all of that across the seam into
215/// the caller's own metadata store is a different decision from "an imported
216/// colour should be visible".
217#[wasm_bindgen]
218pub fn scene_metadata_colors_json() -> String {
219 SCENE_METADATA.with(|store| {
220 let mut out = serde_json::Map::new();
221 for (name, record) in store.borrow().iter() {
222 if let Some(color) = record
223 .get(crate::COLOR_METADATA_KEY)
224 .and_then(|value| value.as_str())
225 {
226 out.insert(name.clone(), serde_json::Value::String(color.to_string()));
227 }
228 }
229 serde_json::Value::Object(out).to_string()
230 })
231}
232
233/// The OWN metadata record for a name (no inheritance) — the snapshot capture
234/// read (`io/snapshot.rs` embeds the record verbatim so a restored part carries
235/// its stamped topology metadata). `None` when the name has no record.
236pub(crate) fn own_record(name: &str) -> Option<serde_json::Map<String, serde_json::Value>> {
237 SCENE_METADATA.with(|store| store.borrow().get(name).cloned())
238}
239
240/// Batch-read RESOLVED records for a list of names (the per-solid display
241/// read: one call per materialization, not one per face).
242#[wasm_bindgen]
243pub fn topo_metadata_for_names_json(names_json: &str) -> Result<String, JsValue> {
244 let names: Vec<String> = serde_json::from_str(names_json)
245 .map_err(|error| JsValue::from_str(&format!("topo_metadata_for_names: {error}")))?;
246 SCENE_METADATA.with(|store| {
247 let store = store.borrow();
248 let mut out = serde_json::Map::new();
249 for name in names {
250 let resolved = resolve(&store, &name);
251 if !resolved.is_empty() {
252 out.insert(name, serde_json::Value::Object(resolved));
253 }
254 }
255 Ok(serde_json::Value::Object(out).to_string())
256 })
257}
258
259// BREP private tests: 5b962baf61e8b5f3