use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::BTreeMap;
use crate::feature_pipeline::{
execute_feature, scene_metadata, sheet_metal, Env, FeatureDescriptor, HistoryRequest,
PortRecord, SceneMap,
};
use crate::BrepSolid;
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PartsLibraryEntry {
#[serde(rename = "sourceKey", default)]
pub source_key: String,
#[serde(rename = "sourceSignature", default)]
pub source_signature: String,
#[serde(default)]
pub document: serde_json::Value,
#[serde(default)]
pub snapshot: String,
#[serde(rename = "snapshotProducer", default, skip_serializing_if = "String::is_empty")]
pub snapshot_producer: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub ports: BTreeMap<String, PortRecord>,
#[serde(skip)]
pub dirty: bool,
#[serde(skip)]
pub doc_hash: u64,
}
pub type PartsLibraryMap = BTreeMap<String, PartsLibraryEntry>;
pub(crate) fn kernel_source_stamp() -> Option<&'static str> {
option_env!("BREP_KERNEL_SOURCE_HASH")
}
thread_local! {
static ROOT: RefCell<PartsLibraryMap> = RefCell::new(BTreeMap::new());
static STACK: RefCell<Vec<PartsLibraryMap>> = const { RefCell::new(Vec::new()) };
static REVISION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
fn bump_revision() {
REVISION.with(|revision| revision.set(revision.get().wrapping_add(1)));
}
pub fn parts_library_revision() -> u64 {
REVISION.with(std::cell::Cell::get)
}
pub fn stable_json_hash(value: &serde_json::Value) -> u64 {
use std::hash::{Hash, Hasher};
fn walk(value: &serde_json::Value, hasher: &mut impl Hasher) {
match value {
serde_json::Value::Null => 0u8.hash(hasher),
serde_json::Value::Bool(flag) => {
1u8.hash(hasher);
flag.hash(hasher);
}
serde_json::Value::Number(number) => {
2u8.hash(hasher);
number.to_string().hash(hasher);
}
serde_json::Value::String(text) => {
3u8.hash(hasher);
text.hash(hasher);
}
serde_json::Value::Array(items) => {
4u8.hash(hasher);
for item in items {
walk(item, hasher);
}
}
serde_json::Value::Object(map) => {
5u8.hash(hasher);
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
key.hash(hasher);
walk(&map[key], hasher);
}
}
}
}
let mut hasher = std::collections::hash_map::DefaultHasher::new();
walk(value, &mut hasher);
hasher.finish()
}
pub(crate) fn active_entry(part_name: &str) -> Option<PartsLibraryEntry> {
STACK.with(|stack| {
let stack = stack.borrow();
if let Some(top) = stack.last() {
return top.get(part_name).cloned();
}
ROOT.with(|root| root.borrow().get(part_name).cloned())
})
}
pub(crate) fn heal_entry(part_name: &str, snapshot: String, ports: BTreeMap<String, PortRecord>) {
let heal = |map: &mut PartsLibraryMap| {
if let Some(entry) = map.get_mut(part_name) {
entry.snapshot = snapshot.clone();
entry.snapshot_producer = kernel_source_stamp().unwrap_or_default().to_string();
entry.ports = ports.clone();
entry.dirty = false;
}
};
STACK.with(|stack| {
let mut stack = stack.borrow_mut();
if let Some(top) = stack.last_mut() {
heal(top);
} else {
ROOT.with(|root| heal(&mut root.borrow_mut()));
bump_revision();
}
});
}
pub(crate) fn clear_all() {
let had_entries = ROOT.with(|root| {
let mut root = root.borrow_mut();
let had = !root.is_empty();
root.clear();
had
});
STACK.with(|stack| stack.borrow_mut().clear());
if had_entries {
bump_revision();
}
}
pub(crate) fn is_acomp_type(feature_type: &str) -> bool {
matches!(feature_type, "ACOMP" | "ASSEMBLY COMPONENT")
}
pub(crate) fn ingest(request_map: &PartsLibraryMap) {
if request_map.is_empty() {
return;
}
let seeded = ROOT.with(|root| {
let mut root = root.borrow_mut();
let mut seeded = false;
for (name, incoming) in request_map {
if root.contains_key(name) {
continue;
}
let mut entry = incoming.clone();
stamp_document_loop_ids(&mut entry.document);
entry.doc_hash = stable_json_hash(&entry.document);
entry.dirty = needs_port_heal(&entry);
root.insert(name.clone(), entry);
seeded = true;
}
seeded
});
if seeded {
bump_revision();
}
}
fn needs_port_heal(entry: &PartsLibraryEntry) -> bool {
entry.ports.is_empty() && document_declares_ports(&entry.document)
}
fn document_declares_ports(document: &serde_json::Value) -> bool {
match document {
serde_json::Value::Object(map) => {
map.get(crate::feature_pipeline::ports::PORTS_BLOCK)
.and_then(serde_json::Value::as_array)
.is_some_and(|block| !block.is_empty())
|| matches!(
map.get("type").and_then(serde_json::Value::as_str),
Some("WP" | "WAYPOINT")
)
|| map.values().any(document_declares_ports)
}
serde_json::Value::Array(items) => items.iter().any(document_declares_ports),
_ => false,
}
}
fn identity(entry: &PartsLibraryEntry) -> (&str, &str, u64) {
(&entry.source_key, &entry.source_signature, entry.doc_hash)
}
pub fn install_parts_library(incoming: &PartsLibraryMap) -> bool {
let mut changed = false;
ROOT.with(|root| {
let mut root = root.borrow_mut();
root.retain(|name, _| {
let keep = incoming.contains_key(name);
changed |= !keep;
keep
});
for (name, entry) in incoming {
let mut entry = entry.clone();
stamp_document_loop_ids(&mut entry.document);
entry.doc_hash = stable_json_hash(&entry.document);
entry.dirty = needs_port_heal(&entry);
match root.get(name) {
Some(resident) if identity(resident) == identity(&entry) => continue,
Some(resident) if same_stamped_build(&resident.document, &entry.document) => {
entry.snapshot = resident.snapshot.clone();
entry.snapshot_producer = resident.snapshot_producer.clone();
entry.ports = resident.ports.clone();
entry.dirty = resident.dirty || needs_port_heal(&entry);
}
Some(_) => entry.dirty = true,
None => {}
}
root.insert(name.clone(), entry);
changed = true;
}
});
if changed {
bump_revision();
}
changed
}
pub fn parts_library_map() -> PartsLibraryMap {
ROOT.with(|root| root.borrow().clone())
}
pub fn missing_library_parts(request: &HistoryRequest) -> Vec<String> {
let mut missing = Vec::new();
ROOT.with(|root| {
let root = root.borrow();
for descriptor in &request.features {
if !is_acomp_type(&descriptor.feature_type) {
continue;
}
let Some(name) = descriptor
.input_params
.get("partName")
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|name| !name.is_empty())
else {
continue;
};
if !root.contains_key(name) && !missing.iter().any(|seen| seen == name) {
missing.push(name.to_string());
}
}
});
missing
}
pub(crate) fn mix_descriptor_fingerprint(
descriptor: &FeatureDescriptor,
fingerprint: u64,
) -> (u64, bool) {
if !is_acomp_type(&descriptor.feature_type) {
return (fingerprint, false);
}
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
fingerprint.hash(&mut hasher);
let part_name = descriptor
.input_params
.get("partName")
.and_then(|value| value.as_str())
.unwrap_or("");
part_name.hash(&mut hasher);
let mut force_dirty = false;
match active_entry(part_name) {
Some(entry) => {
1u8.hash(&mut hasher);
entry.source_key.hash(&mut hasher);
entry.source_signature.hash(&mut hasher);
entry.doc_hash.hash(&mut hasher);
force_dirty = entry.dirty;
}
None => 0u8.hash(&mut hasher),
}
(hasher.finish(), force_dirty)
}
pub(crate) fn gc_after_rebuild(request: &HistoryRequest) {
let referenced: std::collections::BTreeSet<String> = request
.features
.iter()
.filter(|descriptor| is_acomp_type(&descriptor.feature_type))
.filter_map(|descriptor| {
descriptor
.input_params
.get("partName")
.and_then(|value| value.as_str())
.map(|name| name.trim().to_string())
})
.collect();
let dropped = ROOT.with(|root| {
let mut root = root.borrow_mut();
let before = root.len();
root.retain(|name, _| referenced.contains(name));
before != root.len()
});
if dropped {
bump_revision();
}
}
fn run_isolated_document(
document: &serde_json::Value,
pmi: Option<&mut Option<crate::feature_pipeline::pmi::PmiReport>>,
) -> Result<IsolatedRun, String> {
let mut request: HistoryRequest = serde_json::from_value(document.clone())
.map_err(|error| format!("embedded part document does not parse: {error}"))?;
request.stop_at_id = None;
request.stop_before_id = None;
STACK.with(|stack| {
let mut library = std::mem::take(&mut request.parts_library);
for entry in library.values_mut() {
entry.dirty = needs_port_heal(entry);
}
stack.borrow_mut().push(library);
});
let electronics_part = ["symbol", "pads"]
.iter()
.any(|key| document.get(*key).is_some_and(serde_json::Value::is_object))
|| !request.ports.is_empty();
let outcome = run_isolated_features(
&request,
pmi,
electronics_part,
crate::feature_pipeline::ports::document_is_boundary(document),
);
STACK.with(|stack| {
stack.borrow_mut().pop();
});
outcome
}
type IsolatedRun = (Vec<(String, BrepSolid)>, BTreeMap<String, PortRecord>);
fn run_isolated_features(
request: &HistoryRequest,
pmi: Option<&mut Option<crate::feature_pipeline::pmi::PmiReport>>,
electronics_part: bool,
boundary: bool,
) -> Result<IsolatedRun, String> {
let env = Env::build(&request.expressions, &request.configurator).unwrap_or_else(Env::poisoned);
let mut scene = SceneMap::default();
crate::feature_pipeline::ports::seed_history_run(request, &mut scene, &env);
let mut results = Vec::with_capacity(request.features.len());
let mut all_handles: Vec<u32> = Vec::new();
let mut error: Option<String> = None;
for descriptor in &request.features {
let result = execute_feature(descriptor, &env, &scene);
scene.apply(&result);
all_handles.extend(result.added.iter().map(|added| added.handle));
if !result.id.is_empty() {
let mut seed = serde_json::Map::new();
seed.insert(
"sourceFeatureId".into(),
serde_json::Value::String(result.id.clone()),
);
for added in &result.added {
for (_, face_name) in &added.face_names {
scene_metadata::merge_record(face_name, &seed, false);
}
for (_, edge_name) in &added.edge_names {
scene_metadata::merge_record(edge_name, &seed, false);
}
}
}
if let Some(message) = &result.error {
error = Some(format!("feature '{}': {message}", result.id));
break;
}
results.push(result);
}
let mut members = Vec::new();
if error.is_none() {
for result in &results {
for added in &result.added {
if scene.resolve_solid(&added.name) == Some(added.handle) {
let solid =
crate::with_registered_solid_str(added.handle, |solid| Ok(solid.clone()))?;
members.push((added.name.clone(), solid));
}
}
}
}
if let Some(out) = pmi {
*out = crate::feature_pipeline::pmi::finish_history_run(request, &scene, &env);
}
let declared: Vec<String> = crate::feature_pipeline::ports::finish_history_run(
request,
&mut scene,
&env,
)
.report
.map(|report| report.points.into_iter().map(|row| row.address).collect())
.unwrap_or_default();
for handle in all_handles {
sheet_metal::remove_tree(handle);
crate::free_registered_solid(handle);
}
let scene_ports: BTreeMap<String, PortRecord> = scene
.ports
.iter()
.map(|(id, port)| (id.clone(), port.clone()))
.collect();
let ports = crate::feature_pipeline::ports::export_ports(&scene_ports, &declared, boundary);
match error {
Some(message) => Err(message),
None if members.is_empty() && !electronics_part =>
Err("embedded part document produced no solids".into()),
None => Ok((members, ports)),
}
}
pub(crate) fn rebuild_snapshot(
document: &serde_json::Value,
) -> Result<(String, BTreeMap<String, PortRecord>), String> {
let saved = scene_metadata::take_store();
let outcome = run_isolated_document(document, None).and_then(|(members, ports)| {
let refs: Vec<(&str, &BrepSolid)> = members
.iter()
.map(|(name, solid)| (name.as_str(), solid))
.collect();
crate::snapshot_solids(&refs).map(|snapshot| (snapshot, ports))
});
scene_metadata::restore_store(saved);
outcome
}
pub(crate) fn document_pmi(
document: &serde_json::Value,
) -> Option<(crate::feature_pipeline::pmi::PmiState, crate::feature_pipeline::pmi::PmiReport)> {
let state: crate::feature_pipeline::pmi::PmiState = serde_json::from_value(document.get("pmi")?.clone()).ok()?;
if state.views.is_empty() {
return None;
}
let saved = scene_metadata::take_store();
let mut report = None;
let outcome = run_isolated_document(document, Some(&mut report));
scene_metadata::restore_store(saved);
outcome.ok()?;
report.map(|report| (state, report))
}
fn unique_name(root: &PartsLibraryMap, requested: &str) -> String {
if !root.contains_key(requested) {
return requested.to_string();
}
for counter in 2.. {
let candidate = format!("{requested}-{counter}");
if !root.contains_key(&candidate) {
return candidate;
}
}
unreachable!("counter loop is unbounded")
}
fn stamp_document_loop_ids(document: &mut serde_json::Value) {
let Some(features) = document
.get_mut("features")
.and_then(serde_json::Value::as_array_mut)
else {
return;
};
for feature in features {
if feature.get("type").and_then(serde_json::Value::as_str) != Some("S") {
continue;
}
let Some(sketch) = feature
.get_mut("persistentData")
.and_then(|data| data.get_mut("sketch"))
else {
continue;
};
crate::feature_pipeline::assign_sketch_loop_ids(sketch);
}
}
#[wasm_bindgen]
pub fn add_part_to_library(
name: &str,
source_key: &str,
source_signature: &str,
document_json: &str,
) -> Result<String, JsValue> {
add_part_to_library_impl(name, source_key, source_signature, document_json)
.map_err(|error| JsValue::from_str(&error))
}
pub fn add_part_to_library_impl(
name: &str,
source_key: &str,
source_signature: &str,
document_json: &str,
) -> Result<String, String> {
let requested = name.trim();
if requested.is_empty() {
return Err("add_part_to_library: empty part name".to_string());
}
let reused = ROOT.with(|root| {
root.borrow().iter().find_map(|(entry_name, entry)| {
(entry.source_key == source_key && entry.source_signature == source_signature)
.then(|| entry_name.clone())
})
});
if let Some(entry_name) = reused {
return Ok(entry_name);
}
let mut document: serde_json::Value = serde_json::from_str(document_json)
.map_err(|error| format!("add_part_to_library: {error}"))?;
stamp_document_loop_ids(&mut document);
let (snapshot, ports) = rebuild_snapshot(&document)
.map_err(|error| format!("add_part_to_library: {error}"))?;
let entry = PartsLibraryEntry {
source_key: source_key.to_string(),
source_signature: source_signature.to_string(),
doc_hash: stable_json_hash(&document),
document,
snapshot,
snapshot_producer: kernel_source_stamp().unwrap_or_default().to_string(),
ports,
dirty: false,
};
let final_name = ROOT.with(|root| {
let mut root = root.borrow_mut();
let final_name = unique_name(&root, requested);
root.insert(final_name.clone(), entry);
final_name
});
bump_revision();
Ok(final_name)
}
#[wasm_bindgen]
pub fn refresh_library_entry(
name: &str,
source_signature: &str,
document_json: &str,
) -> Result<(), JsValue> {
refresh_library_entry_impl(name, source_signature, document_json)
.map_err(|error| JsValue::from_str(&error))
}
pub fn refresh_library_entry_impl(
name: &str,
source_signature: &str,
document_json: &str,
) -> Result<(), String> {
let mut document: serde_json::Value = serde_json::from_str(document_json)
.map_err(|error| format!("refresh_library_entry: {error}"))?;
stamp_document_loop_ids(&mut document);
ROOT.with(|root| {
let mut root = root.borrow_mut();
let Some(entry) = root.get_mut(name) else {
return Err(format!(
"refresh_library_entry: no parts-library entry named '{name}'"
));
};
let rebuild = !same_stamped_build(&entry.document, &document);
entry.source_signature = source_signature.to_string();
entry.doc_hash = stable_json_hash(&document);
entry.document = document;
if rebuild {
entry.ports.clear(); entry.dirty = true;
}
Ok(())
})?;
bump_revision();
Ok(())
}
pub fn same_build(a: &serde_json::Value, b: &serde_json::Value) -> bool {
let stamped = |document: &serde_json::Value| {
let mut document = document.clone();
stamp_document_loop_ids(&mut document);
document
};
same_stamped_build(&stamped(a), &stamped(b))
}
fn same_stamped_build(a: &serde_json::Value, b: &serde_json::Value) -> bool {
matches!((build_identity(a), build_identity(b)), (Some(a), Some(b)) if a == b)
}
fn build_identity(document: &serde_json::Value) -> Option<u64> {
let mut request: HistoryRequest = serde_json::from_value(document.clone()).ok()?;
request.stop_at_id = None;
request.stop_before_id = None;
Some(stable_json_hash(&serde_json::to_value(&request).ok()?))
}
#[wasm_bindgen]
pub fn parts_library_json() -> String {
ROOT.with(|root| {
serde_json::to_string(&*root.borrow()).unwrap_or_else(|_| "{}".to_string())
})
}