use std::collections::BTreeMap;
use crate::feature_pipeline::component::{self, create_component};
use crate::feature_pipeline::features::{common, port};
use crate::feature_pipeline::{
parts_library, scene_metadata, FeatureContext, FeatureResult, PortRecord,
};
use crate::{restore_solids, AffineTransform, RestoredSnapshot};
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
if !crate::is_component_reference(&ctx.id) {
return Err(format!(
"component feature id '{}' must have the form ACOMP<digits>",
ctx.id
));
}
let part_name = ctx
.string("partName")
.map(|name| name.trim().to_string())
.filter(|name| !name.is_empty())
.ok_or("missing required param `partName` (a parts-library entry name)")?;
let entry = parts_library::active_entry(&part_name).ok_or_else(|| {
format!("part '{part_name}' is not in the parts library")
})?;
let transform_value = ctx.param("transform");
let translate = common::vec3_from_value(
ctx.env,
transform_value.and_then(|value| value.get("translate")),
"transform.translate",
[0.0, 0.0, 0.0],
)?;
let rotate_deg = common::vec3_from_value(
ctx.env,
transform_value.and_then(|value| value.get("rotateEulerDeg")),
"transform.rotateEulerDeg",
[0.0, 0.0, 0.0],
)?;
let matrix = common::compose_trs_matrix(
translate,
[
rotate_deg[0].to_radians(),
rotate_deg[1].to_radians(),
rotate_deg[2].to_radians(),
],
[1.0, 1.0, 1.0],
[0.0, 0.0, 0.0],
);
let transform = AffineTransform::new(matrix)?;
let fixed = match ctx.param("isFixed") {
None | Some(serde_json::Value::Null) => ctx.scene.components.is_empty(),
Some(serde_json::Value::Bool(flag)) => *flag,
Some(other) => return Err(format!("param `isFixed` must be a boolean, found {other}")),
};
let (restored, ports): (RestoredSnapshot, BTreeMap<String, PortRecord>) = if entry.dirty {
heal(&part_name, &entry.document)?
} else {
match restore_solids(&entry.snapshot) {
Ok(snapshot) => (snapshot, entry.ports.clone()),
Err(_) => heal(&part_name, &entry.document)?,
}
};
let members: Vec<(String, crate::BrepSolid)> = restored
.solids
.into_iter()
.map(|solid| (solid.name, solid.solid))
.collect();
let source = serde_json::json!({
"sourceKey": entry.source_key,
"sourceSignature": entry.source_signature,
});
let (mut record, added) =
create_component(&ctx.id, &part_name, fixed, source, transform, members)?;
let posed_ports = component::attach_component_ports(&mut record, &ports);
for (name, mut metadata_record) in restored.metadata {
if let Some(serde_json::Value::String(parent)) = metadata_record.get_mut("inheritsFrom") {
*parent = component::namespaced(&ctx.id, parent);
}
scene_metadata::set_record(&component::namespaced(&ctx.id, &name), metadata_record);
}
let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
result.added = added;
result.components = vec![record];
for (id, posed) in posed_ports {
port::publish_port(&mut result, &id, posed)?;
}
Ok(result)
}
fn heal(
part_name: &str,
document: &serde_json::Value,
) -> Result<(RestoredSnapshot, BTreeMap<String, PortRecord>), String> {
let (payload, ports) = parts_library::rebuild_snapshot(document).map_err(|error| {
format!("part '{part_name}': snapshot unreadable and document re-execution failed: {error}")
})?;
let restored = restore_solids(&payload)
.map_err(|error| format!("part '{part_name}': rebuilt snapshot did not decode: {error}"))?;
parts_library::heal_entry(part_name, payload, ports.clone());
Ok((restored, ports))
}
pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
false
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "ACOMP",
"shortName": "ACOMP",
"longName": "Assembly Component",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the component instance"
},
"partName": {
"type": "string",
"default_value": null,
"hint": "parts-library entry this instance places (filled by the insert flow)"
},
"transform": {
"type": "transform",
"default_value": { "translate": [0, 0, 0], "rotateEulerDeg": [0, 0, 0] },
"hint": "rigid instance pose: translate [x,y,z] + intrinsic-XYZ Euler degrees (expression-capable)"
},
"isFixed": {
"type": "boolean",
"default_value": false,
"label": "Fixed",
"hint": "grounded — the solver never moves it; the first component of an empty assembly is auto-fixed when unset"
}
}
})
}