use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use crate::{NurbsCurve, Vec3};
pub(crate) mod component;
pub use component::ComponentRecord;
pub(crate) mod parts_library;
pub use parts_library::{
add_part_to_library, install_parts_library, missing_library_parts, parts_library_json,
parts_library_map, parts_library_revision, refresh_library_entry, PartsLibraryEntry,
PartsLibraryMap, stable_json_hash,
};
pub mod assembly;
pub mod wire_harness;
pub mod pmi;
pub(crate) use features::import3d::imported_solid_names;
pub use assembly::{AssemblyState, ConstraintEntry};
mod expression;
#[path = "features/mod.rs"]
pub(crate) mod features;
pub use features::common::{first_reference_name, reference_names};
pub(crate) use features::transform::bbox_center as transform_bbox_center;
pub use features::import3d::{native_import_payload, native_import_payload_with_appearance};
pub(crate) mod scene_metadata;
pub use scene_metadata::IsolatedSceneMetadata;
pub use scene_metadata::scene_metadata_colors_json;
mod schema;
pub use schema::feature_schema_catalogue;
pub mod context_offer;
pub use context_offer::{feature_context_applicable, SelectionProbe};
#[path = "sheet_metal/mod.rs"]
mod sheet_metal;
pub use sheet_metal::{flat_pattern_dxf, flat_pattern_svg, is_sheet_metal_handle};
pub use expression::Env;
pub use features::sketch::assign_sketch_loop_ids;
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureDescriptor {
#[serde(rename = "type", default)]
pub feature_type: String,
#[serde(rename = "inputParams", default)]
pub input_params: serde_json::Value,
#[serde(rename = "persistentData", default)]
pub persistent_data: serde_json::Value,
#[serde(default)]
pub timestamp: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryRequest {
#[serde(default, deserialize_with = "de_string_lenient")]
pub expressions: String,
#[serde(default)]
pub configurator: serde_json::Value,
#[serde(default)]
pub features: Vec<FeatureDescriptor>,
#[serde(default, rename = "stopAtId")]
pub stop_at_id: Option<String>,
#[serde(default, rename = "stopBeforeId")]
pub stop_before_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assembly: Option<assembly::AssemblyState>,
#[serde(default, rename = "wireHarness", skip_serializing_if = "Option::is_none")]
pub wire_harness: Option<wire_harness::WireHarnessState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pmi: Option<pmi::PmiState>,
#[serde(default = "default_display_lod", rename = "displayLod")]
pub display_lod: f64,
#[serde(default, rename = "partsLibrary")]
pub parts_library: parts_library::PartsLibraryMap,
}
fn default_display_lod() -> f64 {
1.0
}
fn de_string_lenient<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
Ok(value.unwrap_or_default())
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct AddedSolid {
pub handle: u32,
pub name: String,
pub face_names: Vec<(u64, String)>,
pub edge_names: Vec<(u64, String)>,
#[serde(default)]
pub face_groups: Vec<(String, Vec<String>)>,
#[serde(default)]
pub edge_groups: Vec<(String, Vec<String>)>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ScenePoint {
pub position: Vec3,
pub construction: bool,
}
impl ScenePoint {
pub fn model(position: Vec3) -> Self {
Self { position, construction: false }
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FeatureResult {
pub id: String,
pub feature_type: String,
pub added: Vec<AddedSolid>,
pub removed: Vec<String>,
pub error: Option<String>,
pub unresolved: Vec<String>,
pub reused: bool,
#[serde(skip)]
pub profiles: Vec<(String, SketchProfile)>,
#[serde(skip)]
pub frames: Vec<(String, Frame)>,
#[serde(skip)]
pub axes: Vec<(String, Axis)>,
#[serde(skip)]
pub paths: Vec<(String, Vec<NurbsCurve>)>,
#[serde(skip)]
pub path_segment_names: Vec<(String, Vec<Option<String>>)>,
#[serde(skip)]
pub points: Vec<(String, ScenePoint)>,
#[serde(skip)]
pub ports: Vec<(String, PortRecord)>,
#[serde(skip)]
pub components: Vec<ComponentRecord>,
}
impl FeatureResult {
pub fn empty(id: impl Into<String>, feature_type: impl Into<String>) -> Self {
Self {
id: id.into(),
feature_type: feature_type.into(),
added: Vec::new(),
removed: Vec::new(),
error: None,
unresolved: Vec::new(),
reused: false,
profiles: Vec::new(),
frames: Vec::new(),
axes: Vec::new(),
paths: Vec::new(),
path_segment_names: Vec::new(),
points: Vec::new(),
ports: Vec::new(),
components: Vec::new(),
}
}
pub fn error(
id: impl Into<String>,
feature_type: impl Into<String>,
message: impl Into<String>,
) -> Self {
let mut result = Self::empty(id, feature_type);
result.error = Some(message.into());
result
}
pub fn pass_through(id: impl Into<String>, feature_type: impl Into<String>) -> Self {
Self::empty(id, feature_type)
}
pub fn not_yet_migrated(id: impl Into<String>, feature_type: impl Into<String>) -> Self {
let feature_type = feature_type.into();
let message = format!("feature type '{feature_type}' is not yet migrated to the Rust pipeline");
Self::error(id, feature_type, message)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct HistoryResult {
pub results: Vec<FeatureResult>,
#[serde(skip)]
pub timings: Vec<(String, f64)>,
#[serde(skip)]
pub wire_harness: Option<wire_harness::WireHarnessReport>,
#[serde(skip)]
pub pmi: Option<pmi::PmiReport>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct FaceRef {
pub handle: u32,
pub face_id: u64,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct EdgeRef {
pub handle: u32,
pub edge_id: u64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SketchProfile {
pub origin: Vec3,
pub x_axis: Vec3,
pub y_axis: Vec3,
pub z_axis: Vec3,
pub regions: Vec<Vec<ProfileLoop>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProfileLoop {
pub curves: Vec<NurbsCurve>,
pub edge_names: Vec<Option<String>>,
#[serde(default)]
pub loop_id: Option<u64>,
}
impl ProfileLoop {
pub fn key(&self) -> Option<String> {
self.loop_id.map(|id| format!("L{id}"))
}
}
pub fn annotate_rename(scene: &SceneMap, name: &str) -> String {
let candidates = rename_candidates(scene, name);
if candidates.is_empty() {
return name.to_string();
}
format!("{name} (renamed: {})", candidates.join(", "))
}
fn rename_candidates(scene: &SceneMap, name: &str) -> Vec<String> {
let stale = name.rsplit_once('[').map_or(name, |(head, tail)| {
if tail.ends_with(']') && tail[..tail.len() - 1].chars().all(|c| c.is_ascii_digit()) {
head
} else {
name
}
});
let cap = ["_START", "_END"]
.into_iter()
.find_map(|suffix| stale.strip_suffix(suffix).map(|stem| (stem.to_string(), suffix)));
let hole = stale
.rsplit_once(":HOLE:")
.map(|(owner, _)| format!("{owner}:HOLE:"));
let mut candidates: Vec<String> = scene
.faces
.keys()
.filter(|live| {
if let Some((stem, suffix)) = &cap {
if let Some(rest) = live.strip_prefix(stem.as_str()) {
return rest.starts_with(':')
&& rest.ends_with(suffix)
&& rest.len() > suffix.len() + 1
&& !rest[1..rest.len() - suffix.len()].contains(':');
}
}
if let Some(prefix) = &hole {
return live.starts_with(prefix.as_str()) && live.as_str() != stale;
}
false
})
.cloned()
.collect();
candidates.sort();
candidates
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Frame {
pub origin: Vec3,
pub x_axis: Vec3,
pub y_axis: Vec3,
pub z_axis: Vec3,
}
impl Frame {
pub fn from_origin_normal(origin: Vec3, normal: Vec3) -> Result<Self, String> {
let z_axis = normal.normalized()?;
let world_up = Vec3::new(0.0, 1.0, 0.0);
let ref_up = if z_axis.dot(world_up).abs() > 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
world_up
};
let x_axis = ref_up.cross(z_axis).normalized()?;
let y_axis = z_axis.cross(x_axis).normalized()?;
Ok(Frame {
origin,
x_axis,
y_axis,
z_axis,
})
}
pub fn to_3d(&self, u: f64, v: f64) -> Vec3 {
self.origin
.add(self.x_axis.scale(u))
.add(self.y_axis.scale(v))
}
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Axis {
pub point: Vec3,
pub direction: Vec3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PortKind {
Termination,
Waypoint,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PortRecord {
pub point: Vec3,
pub direction: Vec3,
pub kind: PortKind,
pub extension: f64,
pub display_length: f64,
pub label: String,
}
impl PortRecord {
pub fn side_direction(&self, side: wire_harness::PortSide) -> Vec3 {
match side {
wire_harness::PortSide::A => self.direction,
wire_harness::PortSide::B => self.direction.scale(-1.0),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SceneMap {
pub solids: HashMap<String, u32>,
pub faces: HashMap<String, FaceRef>,
pub edges: HashMap<String, EdgeRef>,
pub face_groups: HashMap<String, Vec<String>>,
pub edge_groups: HashMap<String, Vec<String>>,
pub profiles: HashMap<String, SketchProfile>,
pub frames: HashMap<String, Frame>,
pub axes: HashMap<String, Axis>,
pub paths: HashMap<String, Vec<NurbsCurve>>,
pub path_segment_names: HashMap<String, Vec<Option<String>>>,
pub points: HashMap<String, ScenePoint>,
pub ports: HashMap<String, PortRecord>,
pub components: component::ComponentMap,
pub name_collisions: Vec<String>,
}
impl SceneMap {
pub fn resolve_solid(&self, name: &str) -> Option<u32> {
self.solids.get(name).copied()
}
pub fn resolve_face(&self, name: &str) -> Option<FaceRef> {
if let Some(face) = self.faces.get(name) {
return Some(*face);
}
match self.resolve_face_group(name).as_slice() {
[only] => Some(*only),
_ => None,
}
}
pub fn resolve_face_group(&self, name: &str) -> Vec<FaceRef> {
if let Some(face) = self.faces.get(name) {
return vec![*face];
}
self.face_groups
.get(name)
.map(|members| {
members
.iter()
.filter_map(|member| self.faces.get(member).copied())
.collect()
})
.unwrap_or_default()
}
pub fn resolve_edge_group(&self, name: &str) -> Vec<EdgeRef> {
if let Some(edge) = self.edges.get(name) {
return vec![*edge];
}
self.edge_groups
.get(name)
.map(|members| {
members
.iter()
.filter_map(|member| self.edges.get(member).copied())
.collect()
})
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn resolve_edge(&self, name: &str) -> Option<EdgeRef> {
if let Some(edge) = self.edges.get(name) {
return Some(*edge);
}
match self.resolve_edge_group(name).as_slice() {
[only] => Some(*only),
_ => None,
}
}
pub fn resolve_profile(&self, name: &str) -> Option<&SketchProfile> {
if let Some(profile) = self.profiles.get(name) {
return Some(profile);
}
name.strip_suffix(":PROFILE")
.and_then(|base| self.profiles.get(base))
}
pub fn resolve_frame(&self, name: &str) -> Option<Frame> {
self.frames.get(name).copied()
}
pub fn resolve_axis(&self, name: &str) -> Option<Axis> {
self.axes.get(name).copied()
}
pub fn resolve_path(&self, name: &str) -> Option<&Vec<NurbsCurve>> {
if let Some(path) = self.paths.get(name) {
return Some(path);
}
name.strip_suffix(":PROFILE")
.and_then(|base| self.paths.get(base))
}
pub fn resolve_path_segment_names(&self, name: &str) -> Option<&Vec<Option<String>>> {
if let Some(names) = self.path_segment_names.get(name) {
return Some(names);
}
name.strip_suffix(":PROFILE")
.and_then(|base| self.path_segment_names.get(base))
}
#[allow(dead_code)]
pub fn resolve_point(&self, name: &str) -> Option<Vec3> {
self.points.get(name).map(|point| point.position)
}
pub fn resolve_port(&self, name: &str) -> Option<&PortRecord> {
self.ports.get(name)
}
pub fn model_points_with_prefix(&self, prefix: &str) -> Vec<(String, Vec3)> {
self.points
.iter()
.filter(|(name, point)| name.starts_with(prefix) && !point.construction)
.map(|(name, point)| (name.clone(), point.position))
.collect()
}
fn apply(&mut self, result: &FeatureResult) {
for name in &result.removed {
if let Some(record) = self.components.remove(name) {
for member in &record.solids {
if let Some(handle) = self.solids.remove(member) {
self.faces.retain(|_, face| face.handle != handle);
self.edges.retain(|_, edge| edge.handle != handle);
}
}
for port in &record.ports {
self.ports.remove(port);
self.axes.remove(port);
self.frames.remove(port);
self.points.remove(&format!("{port}:Base"));
self.paths.remove(&format!("{port}:PortLine"));
}
}
if let Some(handle) = self.solids.remove(name) {
self.faces.retain(|_, face| face.handle != handle);
self.edges.retain(|_, edge| edge.handle != handle);
}
}
for (name, profile) in &result.profiles {
self.profiles.insert(name.clone(), profile.clone());
}
for (name, frame) in &result.frames {
self.frames.insert(name.clone(), *frame);
}
for (name, axis) in &result.axes {
self.axes.insert(name.clone(), *axis);
}
for (name, names) in &result.path_segment_names {
self.path_segment_names.insert(name.clone(), names.clone());
}
for (name, path) in &result.paths {
self.paths.insert(name.clone(), path.clone());
}
for (name, point) in &result.points {
self.points.insert(name.clone(), *point);
}
for (name, port) in &result.ports {
self.ports.insert(name.clone(), port.clone());
}
for record in &result.components {
self.components.insert(record.id.clone(), record.clone());
}
for added in &result.added {
self.solids.insert(added.name.clone(), added.handle);
}
let resident: std::collections::HashSet<u32> = self.solids.values().copied().collect();
for added in &result.added {
for (face_id, name) in &added.face_names {
self.check_name_collision(name, added.handle, &added.name, &resident, true);
self.faces.insert(
name.clone(),
FaceRef {
handle: added.handle,
face_id: *face_id,
},
);
}
for (edge_id, name) in &added.edge_names {
self.check_name_collision(name, added.handle, &added.name, &resident, false);
self.edges.insert(
name.clone(),
EdgeRef {
handle: added.handle,
edge_id: *edge_id,
},
);
}
for (container, members) in &added.face_groups {
self.face_groups.insert(container.clone(), members.clone());
}
for (container, members) in &added.edge_groups {
self.edge_groups.insert(container.clone(), members.clone());
}
}
}
fn check_name_collision(
&mut self,
name: &str,
handle: u32,
solid_name: &str,
resident: &std::collections::HashSet<u32>,
is_face: bool,
) {
let prev = if is_face {
self.faces.get(name).map(|face| face.handle)
} else {
self.edges.get(name).map(|edge| edge.handle)
};
let Some(prev_handle) = prev else { return };
if prev_handle == handle || !resident.contains(&prev_handle) {
return;
}
let owner = self
.solids
.iter()
.find(|(_, resident_handle)| **resident_handle == prev_handle)
.map(|(owner_name, _)| owner_name.as_str())
.unwrap_or("another resident solid");
let kind = if is_face { "face" } else { "edge" };
self.name_collisions.push(format!(
"naming-contract violation: solid `{solid_name}` {kind} name `{name}` \
collides with resident solid `{owner}` (two live solids must not share a \
face/edge name)."
));
}
}
pub(crate) fn name_collision_error(lines: &[String]) -> String {
let mut message = lines.join("; ");
message.push_str(
" — an operation that COPIES geometry (mirror / pattern / transform-copy / \
any new one) must call `common::namespace_copy_names(&mut solid, suffix)` \
so the copy's faces and edges get UNIQUE names.",
);
message
}
pub struct FeatureContext<'a> {
pub id: String,
pub feature_type: String,
pub params: &'a serde_json::Value,
pub persistent: &'a serde_json::Value,
pub env: &'a Env,
pub scene: &'a SceneMap,
}
impl<'a> FeatureContext<'a> {
pub fn param(&self, key: &str) -> Option<&serde_json::Value> {
self.params.get(key)
}
pub fn number(&self, key: &str) -> Result<f64, String> {
match self.param(key) {
Some(serde_json::Value::Number(number)) => number
.as_f64()
.ok_or_else(|| format!("param `{key}` is not a finite number")),
Some(serde_json::Value::String(source)) => self
.env
.eval(source)
.map_err(|error| format!("param `{key}`: {error}")),
Some(other) => Err(format!(
"param `{key}` must be a number or expression string, found {other}"
)),
None => Err(format!("missing required param `{key}`")),
}
}
#[allow(dead_code)]
pub fn string(&self, key: &str) -> Option<String> {
match self.param(key) {
Some(serde_json::Value::String(text)) => Some(text.clone()),
_ => None,
}
}
pub fn fail(&self, message: impl Into<String>) -> FeatureResult {
FeatureResult::error(self.id.clone(), self.feature_type.clone(), message)
}
}
fn extract_id(params: &serde_json::Value) -> String {
for key in ["id", "featureID"] {
if let Some(serde_json::Value::String(text)) = params.get(key) {
if !text.is_empty() {
return text.clone();
}
}
}
String::new()
}
pub fn execute_feature(
descriptor: &FeatureDescriptor,
env: &Env,
scene: &SceneMap,
) -> FeatureResult {
let id = extract_id(&descriptor.input_params);
let feature_type = descriptor.feature_type.clone();
if let Err(message) = component::enforce_reference_fence(&feature_type, descriptor, scene) {
return FeatureResult::error(id, feature_type, message);
}
let ctx = FeatureContext {
id: id.clone(),
feature_type: feature_type.clone(),
params: &descriptor.input_params,
persistent: &descriptor.persistent_data,
env,
scene,
};
match feature_type.as_str() {
"P.CU" | "CUBE" => features::cube::execute(&ctx),
"P.S" | "SPHERE" => features::sphere::execute(&ctx),
"P.CY" | "CYLINDER" => features::cylinder::execute(&ctx),
"P.CO" | "CONE" => features::cone::execute(&ctx),
"P.T" | "TORUS" => features::torus::execute(&ctx),
"P.PY" | "PYRAMID" => features::pyramid::execute(&ctx),
"E" | "EXTRUDE" => features::extrude::execute(&ctx),
"R" | "REVOLVE" => features::revolve::execute(&ctx),
"LOFT" => features::loft::execute(&ctx),
"SW" | "SWEEP" => features::sweep::execute(&ctx),
"SWP" | "PATH SWEEP" | "PATHSWEEP" => features::path_sweep::execute(&ctx),
"RIB" => features::rib::execute(&ctx),
"TU" | "TUBE" => features::tube::execute(&ctx),
"B" | "BOOLEAN" => features::boolean::execute(&ctx),
"M" | "MIRROR" => features::mirror::execute(&ctx),
"SPL" | "SPLIT" => features::split::execute(&ctx),
"XFORM" | "TRANSFORM" => features::transform::execute(&ctx),
"PATTERN" => features::pattern::execute(&ctx),
"F" | "FILLET" => features::fillet::execute(&ctx),
"CH" | "CHAMFER" => features::chamfer::execute(&ctx),
"O.S" | "OFFSET SHELL" | "OFFSETSHELL" => features::offset_shell::execute(&ctx),
"O.F" | "OFFSET FACE" | "OFFSETFACE" => features::offset_face::execute(&ctx),
"PF" | "PUSHFACE" | "PUSH FACE" => features::push_face::execute(&ctx),
"DF" | "DELETE FACE" | "DELETEFACE" => features::delete_face::execute(&ctx),
"THK" | "THICKEN" => features::thicken::execute(&ctx),
"H" | "HOLE" => features::hole::execute(&ctx),
"IMPORT3D" => features::import3d::execute(&ctx),
"SM.TAB" => features::sheet_metal_tab::execute(&ctx),
"SM.CF" => features::sheet_metal_contour_flange::execute(&ctx),
"SM.F" => features::sheet_metal_flange::execute(&ctx),
"SM.HEM" => features::sheet_metal_hem::execute(&ctx),
"SM.FILLET" => features::sheet_metal_corner::execute_fillet(&ctx),
"SM.CHAMFER" => features::sheet_metal_corner::execute_chamfer(&ctx),
"SM.CUTOUT" => features::sheet_metal_cutout::execute(&ctx),
"SM.UNFOLD" => features::sheet_metal_unfold::execute(&ctx),
"ACOMP" | "ASSEMBLY COMPONENT" => features::assembly_component::execute(&ctx),
"D" | "DATUM" | "DATIUM" => features::datum::execute(&ctx),
"P" | "PLANE" => features::plane::execute(&ctx),
"S" | "SKETCH" => features::sketch::execute(&ctx),
"SP" | "SPLINE" => features::spline::execute(&ctx),
"PORT" => features::port::execute(&ctx),
"HX" | "HELIX" => features::helix::execute(&ctx),
_ => FeatureResult::error(id, feature_type.clone(), format!("unknown feature type '{feature_type}'")),
}
}
struct CachedFeature {
fingerprint: u64,
result: FeatureResult,
consumed: std::collections::BTreeSet<String>,
touched: HashMap<String, ()>,
output_version: u64,
}
thread_local! {
static HISTORY_CACHE: std::cell::RefCell<HashMap<String, CachedFeature>> =
std::cell::RefCell::new(HashMap::new());
}
fn free_entry(entry: &CachedFeature) {
for added in &entry.result.added {
sheet_metal::remove_tree(added.handle);
crate::free_registered_solid(added.handle);
}
}
pub fn clear_history_cache() {
HISTORY_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
for entry in cache.values() {
free_entry(entry);
}
cache.clear();
});
sheet_metal::clear_trees();
wire_harness::clear_cache();
parts_library::clear_all();
}
#[wasm_bindgen]
pub fn clear_history_cache_json() {
clear_history_cache();
}
fn descriptor_fingerprint(descriptor: &FeatureDescriptor, env: &Env) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
descriptor.feature_type.hash(&mut hasher);
hash_evaluated(&descriptor.input_params, env, &mut hasher);
hash_evaluated(&descriptor.persistent_data, env, &mut hasher);
hasher.finish()
}
fn hash_evaluated(value: &serde_json::Value, env: &Env, hasher: &mut impl std::hash::Hasher) {
use std::hash::Hash;
match value {
serde_json::Value::String(text) => {
0u8.hash(hasher);
text.hash(hasher);
if text.len() <= 64 {
if let Ok(number) = env.eval(text) {
1u8.hash(hasher);
number.to_bits().hash(hasher);
}
}
}
serde_json::Value::Array(items) => {
2u8.hash(hasher);
for item in items {
hash_evaluated(item, env, hasher);
}
}
serde_json::Value::Object(map) => {
3u8.hash(hasher);
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
key.hash(hasher);
hash_evaluated(&map[key], env, hasher);
}
}
other => {
4u8.hash(hasher);
other.to_string().hash(hasher);
}
}
}
const SKETCH_REFERENCE_ALIASES: [&str; 2] = [":PROFILE", ":FACE"];
fn scan_consumed(
descriptor: &FeatureDescriptor,
available: &HashMap<String, ()>,
) -> std::collections::BTreeSet<String> {
fn walk(
value: &serde_json::Value,
available: &HashMap<String, ()>,
out: &mut std::collections::BTreeSet<String>,
) {
match value {
serde_json::Value::String(text) => {
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
if available.contains_key(trimmed) {
out.insert(trimmed.to_string());
} else if let Some(base) = SKETCH_REFERENCE_ALIASES
.iter()
.find_map(|alias| trimmed.strip_suffix(alias))
.filter(|base| available.contains_key(*base))
{
out.insert(base.to_string());
}
}
serde_json::Value::Array(items) => {
for item in items {
walk(item, available, out);
}
}
serde_json::Value::Object(map) => {
for item in map.values() {
walk(item, available, out);
}
}
_ => {}
}
}
let mut out = std::collections::BTreeSet::new();
walk(&descriptor.input_params, available, &mut out);
walk(&descriptor.persistent_data, available, &mut out);
out
}
fn feature_output_version(
fingerprint: u64,
consumed: &std::collections::BTreeSet<String>,
name_version: &HashMap<String, u64>,
) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
fingerprint.hash(&mut hasher);
for name in consumed {
name.hash(&mut hasher);
name_version.get(name).copied().unwrap_or(0).hash(&mut hasher);
}
hasher.finish()
}
fn produced_names(result: &FeatureResult) -> Vec<String> {
let mut names = Vec::new();
for added in &result.added {
names.push(added.name.clone());
names.extend(added.face_names.iter().map(|(_, name)| name.clone()));
names.extend(added.edge_names.iter().map(|(_, name)| name.clone()));
names.extend(added.face_groups.iter().map(|(name, _)| name.clone()));
names.extend(added.edge_groups.iter().map(|(name, _)| name.clone()));
}
names.extend(result.profiles.iter().map(|(name, _)| name.clone()));
names.extend(result.frames.iter().map(|(name, _)| name.clone()));
names.extend(result.axes.iter().map(|(name, _)| name.clone()));
names.extend(result.paths.iter().map(|(name, _)| name.clone()));
names.extend(result.points.iter().map(|(name, _)| name.clone()));
names.extend(result.ports.iter().map(|(name, _)| name.clone()));
names.extend(result.components.iter().map(|record| record.id.clone()));
names
}
pub fn execute_history(request: &HistoryRequest) -> HistoryResult {
execute_history_observed(request, &mut |_| true)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HistoryProgress<'a> {
pub index: usize,
pub total: usize,
pub id: &'a str,
pub feature_type: &'a str,
}
pub fn execute_history_observed(
request: &HistoryRequest,
observe: &mut dyn FnMut(HistoryProgress<'_>) -> bool,
) -> HistoryResult {
let env = Env::build(&request.expressions, &request.configurator)
.unwrap_or_else(Env::poisoned);
parts_library::ingest(&request.parts_library);
let request_ids: HashMap<String, ()> = request
.features
.iter()
.map(|descriptor| (extract_id(&descriptor.input_params), ()))
.collect();
HISTORY_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
cache.retain(|id, entry| {
let keep = request_ids.contains_key(id);
if !keep {
free_entry(entry);
}
keep
});
});
let mut scene = SceneMap::default();
let mut results = Vec::with_capacity(request.features.len());
let mut timings: Vec<(String, f64)> = Vec::with_capacity(request.features.len());
let mut changed: HashMap<String, ()> = HashMap::new();
let mut available: HashMap<String, ()> = HashMap::new();
let mut name_version: HashMap<String, u64> = HashMap::new();
let mut seen_ids: HashMap<String, ()> = HashMap::new();
let total = request.features.len();
for (index, descriptor) in request.features.iter().enumerate() {
let id = extract_id(&descriptor.input_params);
if request.stop_before_id.as_deref() == Some(id.as_str()) {
break;
}
let cacheable = !id.is_empty() && seen_ids.insert(id.clone(), ()).is_none();
let fingerprint = descriptor_fingerprint(descriptor, &env);
let (fingerprint, library_dirty) =
parts_library::mix_descriptor_fingerprint(descriptor, fingerprint);
let consumed = scan_consumed(descriptor, &available);
let output_version = feature_output_version(fingerprint, &consumed, &name_version);
let clean = cacheable
&& !library_dirty
&& HISTORY_CACHE.with(|cache| {
cache.borrow().get(&id).is_some_and(|entry| {
entry.fingerprint == fingerprint
&& entry.consumed == consumed
&& consumed.iter().all(|name| !changed.contains_key(name))
&& entry.output_version == output_version
})
});
if clean {
let mut result =
HISTORY_CACHE.with(|cache| cache.borrow().get(&id).unwrap().result.clone());
result.reused = true;
scene.apply(&result);
for name in produced_names(&result) {
available.insert(name.clone(), ());
name_version.insert(name, output_version);
}
results.push(result);
timings.push((id.clone(), 0.0)); if request.stop_at_id.as_deref() == Some(id.as_str()) {
break;
}
continue;
}
if cacheable {
if let Some(old) = HISTORY_CACHE.with(|cache| cache.borrow_mut().remove(&id)) {
free_entry(&old);
changed.extend(old.touched.iter().map(|(name, _)| (name.clone(), ())));
}
}
if !observe(HistoryProgress {
index,
total,
id: &id,
feature_type: &descriptor.feature_type,
}) {
break;
}
let feat_start = web_time::Instant::now();
let mut result = execute_feature(descriptor, &env, &scene);
timings.push((id.clone(), feat_start.elapsed().as_secs_f64() * 1000.0));
let halt = result.error.is_some();
let collisions_before = scene.name_collisions.len();
scene.apply(&result);
if result.error.is_none() && scene.name_collisions.len() > collisions_before {
result.error = Some(name_collision_error(
&scene.name_collisions[collisions_before..],
));
}
if !id.is_empty() {
let mut seed = serde_json::Map::new();
seed.insert("sourceFeatureId".into(), serde_json::Value::String(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);
}
}
}
for name in &mut result.unresolved {
*name = annotate_rename(&scene, name);
}
let produced = produced_names(&result);
for name in &produced {
available.insert(name.clone(), ());
changed.insert(name.clone(), ());
name_version.insert(name.clone(), output_version);
}
for name in &result.removed {
changed.insert(name.clone(), ());
}
if cacheable && !halt {
let mut touched: HashMap<String, ()> =
produced.into_iter().map(|name| (name, ())).collect();
touched.extend(result.removed.iter().map(|name| (name.clone(), ())));
HISTORY_CACHE.with(|cache| {
cache.borrow_mut().insert(
id.clone(),
CachedFeature {
fingerprint,
result: result.clone(),
consumed,
touched,
output_version,
},
);
});
}
results.push(result);
if halt {
break;
}
if request.stop_at_id.as_deref() == Some(id.as_str()) {
break;
}
}
parts_library::gc_after_rebuild(request);
assembly::finish_history_run(request, &mut scene, &env);
let harness = wire_harness::finish_history_run(request, &mut scene, &env);
if let Some(result) = harness.result {
results.push(result);
}
let pmi = pmi::finish_history_run(request, &scene, &env);
HistoryResult {
results,
timings,
wire_harness: harness.report,
pmi,
}
}
#[wasm_bindgen]
pub fn execute_history_json(request_json: &str) -> Result<String, JsValue> {
let request: HistoryRequest = serde_json::from_str(request_json)
.map_err(|error| JsValue::from_str(&format!("execute_history_json: bad request: {error}")))?;
let result = execute_history(&request);
serde_json::to_string(&result).map_err(|error| JsValue::from_str(&error.to_string()))
}