use std::collections::BTreeMap;
use crate::feature_pipeline::features::common::{collect_edge_names, collect_face_names};
use crate::feature_pipeline::{AddedSolid, FeatureDescriptor, PortRecord, SceneMap};
use crate::{transform_brep, AffineTransform, BrepSolid};
#[derive(Debug, Clone)]
pub struct ComponentRecord {
pub id: String,
pub part_name: String,
pub transform: AffineTransform,
pub fixed: bool,
pub source: serde_json::Value,
pub solids: Vec<String>,
pub ports: Vec<String>,
pub nested_constraints: Vec<NestedConstraint>,
pub nested_poses: BTreeMap<String, AffineTransform>,
pub pattern: Option<PatternFollow>,
pub flexible: Option<FlexibleSpec>,
}
#[derive(Debug, Clone)]
pub struct PatternFollow {
pub pattern_id: String,
pub seed: String,
pub delta: AffineTransform,
pub key: i64,
pub driver: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct FlexibleSpec {
pub constraints: Vec<serde_json::Value>,
pub grounded: std::collections::BTreeSet<String>,
pub authored: BTreeMap<String, AffineTransform>,
pub seeds: BTreeMap<String, AffineTransform>,
pub fixed: BTreeMap<String, bool>,
pub nested: BTreeMap<String, FlexibleSpec>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct NestedConstraint {
pub id: String,
#[serde(rename = "type")]
pub constraint_type: String,
pub enabled: bool,
pub status: String,
pub message: String,
pub satisfied: bool,
}
impl NestedConstraint {
pub fn from_entry(entry: &crate::feature_pipeline::assembly::ConstraintEntry) -> Self {
Self {
id: entry.id().to_string(),
constraint_type: entry.constraint_type.clone(),
enabled: entry.enabled,
status: entry.status().to_string(),
message: entry
.persistent("message")
.and_then(|value| value.as_str())
.unwrap_or("")
.to_string(),
satisfied: entry
.persistent("satisfied")
.and_then(|value| value.as_bool())
.unwrap_or(false),
}
}
pub fn namespaced(&self, component_id: &str) -> Self {
Self {
id: namespaced(component_id, &self.id),
..self.clone()
}
}
pub fn is_failure(&self) -> bool {
self.enabled && !self.satisfied
}
}
pub type ComponentMap = BTreeMap<String, ComponentRecord>;
pub fn namespaced(component_id: &str, name: &str) -> String {
format!("{component_id}:{name}")
}
fn namespace_solid_names(solid: &mut BrepSolid, component_id: &str) {
for shell in &mut solid.shells {
for face in &mut shell.faces {
if let Some(name) = &face.name {
face.name = Some(namespaced(component_id, name));
}
}
}
for edge in &mut solid.edges {
if let Some(name) = &edge.name {
edge.name = Some(namespaced(component_id, name));
}
}
}
fn validate_component_id(id: &str) -> Result<(), String> {
if id.is_empty() {
return Err("component id must not be empty".into());
}
if id.contains(':') || id.contains('|') {
return Err(format!("component id '{id}' must not contain ':' or '|'"));
}
Ok(())
}
fn require_rigid(transform: &AffineTransform) -> Result<(), String> {
let m = &transform.elements;
let rows = [[m[0], m[1], m[2]], [m[4], m[5], m[6]], [m[8], m[9], m[10]]];
for i in 0..3 {
for j in i..3 {
let dot: f64 = (0..3).map(|k| rows[i][k] * rows[j][k]).sum();
let expected = if i == j { 1.0 } else { 0.0 };
if (dot - expected).abs() > 1e-8 {
return Err("component transform must be rigid (rotation + translation)".into());
}
}
}
if (transform.determinant3() - 1.0).abs() > 1e-8 {
return Err("component transform must be rigid (no reflection/scale)".into());
}
Ok(())
}
pub(crate) fn compose(a: &AffineTransform, b: &AffineTransform) -> Result<AffineTransform, String> {
let (ma, mb) = (&a.elements, &b.elements);
let mut out = [0.0f64; 16];
for row in 0..4 {
for col in 0..4 {
out[row * 4 + col] = (0..4)
.map(|k| ma[row * 4 + k] * mb[k * 4 + col])
.sum();
}
}
AffineTransform::new(out)
}
#[allow(dead_code)]
pub fn create_component(
id: &str,
part_name: &str,
fixed: bool,
source: serde_json::Value,
transform: AffineTransform,
members: Vec<(String, BrepSolid)>,
) -> Result<(ComponentRecord, Vec<AddedSolid>), String> {
validate_component_id(id)?;
require_rigid(&transform)?;
let mut posed: Vec<(String, BrepSolid)> = Vec::with_capacity(members.len());
for (member_name, solid) in &members {
let mut body = transform_brep(solid, transform, false)
.map_err(|error| format!("component '{id}': member '{member_name}': {error}"))?;
namespace_solid_names(&mut body, id);
posed.push((namespaced(id, member_name), body));
}
let mut added = Vec::with_capacity(posed.len());
let mut solids = Vec::with_capacity(posed.len());
for (name, body) in posed {
let face_names = collect_face_names(&body);
let edge_names = collect_edge_names(&body);
let handle = crate::register_solid_value(body);
solids.push(name.clone());
added.push(AddedSolid {
handle,
name,
face_names,
edge_names,
..AddedSolid::default()
});
}
Ok((
ComponentRecord {
id: id.to_string(),
part_name: part_name.to_string(),
transform,
fixed,
source,
solids,
ports: Vec::new(),
nested_constraints: Vec::new(),
nested_poses: BTreeMap::new(),
pattern: None,
flexible: None,
},
added,
))
}
pub fn pose_port(record: &PortRecord, transform: &AffineTransform) -> PortRecord {
let point = transform.point(record.point);
let tip = transform.point(record.point.add(record.direction));
let direction = tip.sub(point).normalized().unwrap_or(record.direction);
PortRecord {
point,
direction,
..record.clone()
}
}
pub fn attach_component_ports(
record: &mut ComponentRecord,
ports: &BTreeMap<String, PortRecord>,
) -> Vec<(String, PortRecord)> {
let mut posed = Vec::with_capacity(ports.len());
for (local_id, port) in ports {
let id = namespaced(&record.id, local_id);
let port = pose_port(port, &record.transform);
record.ports.push(id.clone());
posed.push((id, port));
}
posed
}
#[allow(dead_code)]
pub fn update_component_transform(
scene: &mut SceneMap,
id: &str,
new_transform: AffineTransform,
) -> Result<(), String> {
require_rigid(&new_transform)?;
let record = scene
.resolve_component(id)
.ok_or_else(|| format!("unknown component '{id}'"))?;
let delta = compose(&new_transform, &record.transform.rigid_inverse()?)?;
let port_ids = record.ports.clone();
let mut posed: Vec<(u32, BrepSolid)> = Vec::with_capacity(record.solids.len());
for name in &record.solids {
let handle = scene
.solids
.get(name)
.copied()
.ok_or_else(|| format!("component '{id}': member '{name}' is not scene-resident"))?;
let body = crate::with_registered_solid_str(handle, |solid| {
transform_brep(solid, delta, false)
})
.map_err(|error| format!("component '{id}': member '{name}': {error}"))?;
posed.push((handle, body));
}
for (handle, body) in posed {
crate::replace_registered_solid(handle, body)?;
}
if let Some(record) = scene.components.get_mut(id) {
record.transform = new_transform;
let prefix = format!("{id}:");
for occurrence in scene.occurrences.values_mut().filter(|o| o.id.starts_with(&prefix)) {
occurrence.transform = compose(&delta, &occurrence.transform)?;
}
super::mark_component_cache_posed(id);
} else if scene.occurrences.contains_key(id) {
let prefix = format!("{id}:");
for occurrence in scene.occurrences.values_mut() {
if occurrence.id == id {
occurrence.transform = new_transform;
} else if occurrence.id.starts_with(&prefix) {
occurrence.transform = compose(&delta, &occurrence.transform)?;
}
}
let owner = id.split(':').next().unwrap_or(id).to_string();
super::mark_component_cache_posed(&owner);
}
for port_id in port_ids {
let Some(port) = scene.ports.get(&port_id) else {
continue;
};
let posed = pose_port(port, &delta);
crate::feature_pipeline::ports::place_scene_port(scene, &port_id, posed)?;
}
Ok(())
}
pub fn reject_component_references<'a>(
scene: &SceneMap,
names: impl IntoIterator<Item = &'a str>,
) -> Result<(), String> {
for name in names {
if let Some(record) = scene.owning_component(name) {
return Err(format!(
"'{name}' belongs to assembly component '{}' — modeling features cannot consume component geometry",
record.id
));
}
}
Ok(())
}
pub fn enforce_reference_fence(
feature_type: &str,
descriptor: &FeatureDescriptor,
scene: &SceneMap,
) -> Result<(), String> {
if scene.components.is_empty() {
return Ok(());
}
if matches!(
feature_type,
"S" | "SKETCH"
| "S3D"
| "SKETCH3D"
| "3D SKETCH"
| "D"
| "DATUM"
| "DATIUM"
| "P"
| "PLANE"
| "ACOMP"
| "ASSEMBLY COMPONENT"
| "APAT"
| "PATTERN COMPONENT"
| "WP"
| "WAYPOINT"
| "SP"
| "SPLINE"
) {
return Ok(());
}
if let Some(name) = first_component_owned(&descriptor.input_params, scene)
.or_else(|| first_component_owned(&descriptor.persistent_data, scene))
{
return reject_component_references(scene, [name]);
}
Ok(())
}
fn first_component_owned<'a>(
value: &'a serde_json::Value,
scene: &SceneMap,
) -> Option<&'a str> {
match value {
serde_json::Value::String(text) => {
let trimmed = text.trim();
(!trimmed.is_empty() && scene.is_component_owned(trimmed)).then_some(trimmed)
}
serde_json::Value::Array(items) => {
items.iter().find_map(|item| first_component_owned(item, scene))
}
serde_json::Value::Object(map) => {
map.values().find_map(|item| first_component_owned(item, scene))
}
_ => None,
}
}
impl SceneMap {
#[allow(dead_code)]
pub fn resolve_component(&self, id: &str) -> Option<&ComponentRecord> {
self.components.get(id).or_else(|| self.occurrences.get(id))
}
pub fn owning_component(&self, name: &str) -> Option<&ComponentRecord> {
if let Some(record) = self.resolve_component(name) {
return Some(record);
}
if let Some(record) = self
.occurrences
.values()
.filter(|record| name.starts_with(&format!("{}:", record.id)))
.max_by_key(|record| record.id.len())
{
return Some(record);
}
let (prefix, _) = name.split_once(':')?;
self.components.get(prefix)
}
pub fn is_component_owned(&self, name: &str) -> bool {
self.owning_component(name).is_some()
}
#[allow(dead_code)]
pub fn iter_components(&self) -> impl Iterator<Item = &ComponentRecord> {
self.components.values()
}
#[allow(dead_code)]
pub fn component_solids(&self, id: &str) -> Vec<(String, u32)> {
let Some(record) = self.resolve_component(id) else {
return Vec::new();
};
record
.solids
.iter()
.filter_map(|name| self.solids.get(name).map(|&handle| (name.clone(), handle)))
.collect()
}
pub fn occurrence_solids(&self, path: &str) -> Vec<(String, u32)> {
if self.resolve_component(path).is_some() {
return self.component_solids(path);
}
let prefix = format!("{path}:");
let mut solids: Vec<(String, u32)> = self
.solids
.iter()
.filter(|(name, _)| name.starts_with(&prefix))
.map(|(name, &handle)| (name.clone(), handle))
.collect();
solids.sort();
solids
}
#[allow(dead_code)]
pub fn component_fixed(&self, id: &str) -> Option<bool> {
self.resolve_component(id).map(|record| record.fixed)
}
#[allow(dead_code)]
pub fn set_component_fixed(&mut self, id: &str, fixed: bool) -> bool {
match self.components.get_mut(id).or(self.occurrences.get_mut(id)) {
Some(record) => {
record.fixed = fixed;
true
}
None => false,
}
}
}