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 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(())
}
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(),
},
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 mut port = pose_port(port, &record.transform);
port.label = namespaced(&record.id, &port.label);
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
.components
.get(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)?;
}
scene
.components
.get_mut(id)
.expect("record fetched above")
.transform = new_transform;
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::features::port::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"
| "D"
| "DATUM"
| "DATIUM"
| "P"
| "PLANE"
| "ACOMP"
| "ASSEMBLY COMPONENT"
| "PORT"
| "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)
}
pub fn owning_component(&self, name: &str) -> Option<&ComponentRecord> {
if let Some(record) = self.components.get(name) {
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.components.get(id) else {
return Vec::new();
};
record
.solids
.iter()
.filter_map(|name| self.solids.get(name).map(|&handle| (name.clone(), handle)))
.collect()
}
#[allow(dead_code)]
pub fn component_fixed(&self, id: &str) -> Option<bool> {
self.components.get(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) {
Some(record) => {
record.fixed = fixed;
true
}
None => false,
}
}
}