use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{scene_metadata, AddedSolid, FeatureContext, FeatureResult};
use crate::{
import_iges, import_step_with_appearance, restore_solids, BodyAppearance, BrepSolid,
ImportedColor, COLOR_METADATA_KEY,
};
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
let feature_name = if ctx.id.is_empty() {
"IMPORT3D".to_string()
} else {
ctx.id.clone()
};
if let Some(native) = ctx.param("nativeBrep").filter(|value| !value.is_null()) {
if ctx.param("stepText").is_some() || ctx.param("igesText").is_some() {
return Err(
"import3d: `nativeBrep` is exclusive — a feature carrying it must not also carry `stepText`/`igesText`"
.into(),
);
}
let payload = native
.as_str()
.ok_or("import3d: param `nativeBrep` must be a base64 snapshot string")?;
return restore_native_bodies(ctx, payload);
}
if let Some(step_text) = ctx.param("stepText").and_then(|v| v.as_str()) {
if step_text.contains("ISO-10303-21") {
let (solids, appearances) = import_step_with_appearance(step_text)
.map_err(|error| format!("import3d: STEP import failed: {error}"))?;
if solids.is_empty() {
return Err("import3d: STEP file contained no importable solids".into());
}
return Ok(add_named_bodies(ctx, solids, &appearances, &feature_name));
}
return Err(
"import3d: only STEP (ISO-10303-21) files are supported (STL/3MF mesh import was removed)"
.into(),
);
}
if let Some(iges_text) = ctx.param("igesText").and_then(|v| v.as_str()) {
let solids = import_iges(iges_text)
.map_err(|error| format!("import3d: IGES import failed: {error}"))?;
if solids.is_empty() {
return Err("import3d: IGES file contained no importable solids".into());
}
return Ok(add_named_bodies(ctx, solids, &[], &feature_name));
}
let cache = ctx.persistent.get("importCache");
if let Some(cache) = cache {
let kind = cache.get("kind").and_then(|v| v.as_str()).unwrap_or("");
if kind == "step-brep" {
let records = cache
.get("kernelSolidRecords")
.and_then(|v| v.as_array())
.ok_or("import3d: importCache has no kernelSolidRecords")?;
let mut solids = Vec::with_capacity(records.len());
for (index, record) in records.iter().enumerate() {
let solid: BrepSolid = serde_json::from_value(record.clone()).map_err(|error| {
format!("import3d: kernel record {index} failed to deserialize: {error}")
})?;
solids.push(solid);
}
if solids.is_empty() {
return Err("import3d: importCache is empty".into());
}
return Ok(add_named_bodies(ctx, solids, &[], &feature_name));
}
return Err(format!(
"import3d: unsupported importCache kind '{kind}' (only 'step-brep')"
));
}
if ctx.persistent.get("failedImportFile").is_some() {
return Err(
"import3d: the failed-import base64 retry lane is not yet migrated to the Rust pipeline"
.into(),
);
}
Err(
"import3d: no model data (no `nativeBrep`/`stepText` param and no `importCache`)".into(),
)
}
fn restore_native_bodies(ctx: &FeatureContext, payload: &str) -> Result<FeatureResult, String> {
let restored = restore_solids(payload)
.map_err(|error| format!("import3d: native payload did not decode: {error}"))?;
if restored.solids.is_empty() {
return Err("import3d: native payload contained no solids".into());
}
let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
for solid in restored.solids {
result.added.push(register_verbatim(solid.solid, &solid.name));
}
for (name, record) in restored.metadata {
scene_metadata::merge_record(&name, &record, true);
}
Ok(result)
}
fn register_verbatim(solid: BrepSolid, name: &str) -> AddedSolid {
let face_names = common::collect_face_names(&solid);
let edge_names = common::collect_edge_names(&solid);
let handle = crate::register_solid_value(solid);
AddedSolid {
handle,
name: name.to_string(),
face_names,
edge_names,
face_groups: Vec::new(),
edge_groups: Vec::new(),
}
}
fn add_named_bodies(
ctx: &FeatureContext,
mut solids: Vec<BrepSolid>,
appearances: &[BodyAppearance],
feature_name: &str,
) -> FeatureResult {
let body_names = stamp_imported_names(&mut solids, feature_name);
let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
for (index, (solid, body_name)) in solids.into_iter().zip(body_names).enumerate() {
let added = common::register_added(solid, &body_name);
if let Some(appearance) = appearances.get(index) {
stamp_appearance(&added.name, &added.face_names, appearance);
}
result.added.push(added);
}
result
}
fn stamp_appearance(body_name: &str, face_names: &[(u64, String)], appearance: &BodyAppearance) {
if let Some(color) = appearance.body {
stamp_color(body_name, color);
}
if appearance.faces.is_empty() || appearance.faces.len() != face_names.len() {
return;
}
for ((_, face_name), color) in face_names.iter().zip(&appearance.faces) {
if let Some(color) = color {
stamp_color(face_name, *color);
}
}
}
fn stamp_color(name: &str, color: ImportedColor) {
let mut record = serde_json::Map::new();
record.insert(
COLOR_METADATA_KEY.to_string(),
serde_json::Value::String(color.to_hex()),
);
scene_metadata::merge_record(name, &record, false);
}
fn stamp_imported_names(solids: &mut [BrepSolid], feature_name: &str) -> Vec<String> {
let body_names = imported_solid_names(solids.len(), feature_name);
let multi_body = solids.len() > 1;
let mut chosen = Vec::with_capacity(solids.len());
for (index, solid) in solids.iter_mut().enumerate() {
let body_name = body_names
.get(index)
.cloned()
.unwrap_or_else(|| feature_name.to_string());
let mut face_index = 0usize;
for shell in &mut solid.shells {
for face in &mut shell.faces {
if multi_body {
face.name = Some(format!("{body_name}_Face_{face_index}"));
} else {
let stem = face
.name
.as_deref()
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
.unwrap_or_else(|| format!("Face_{face_index}"));
face.name = Some(format!("{body_name}_{stem}"));
}
face_index += 1;
}
}
chosen.push(body_name);
}
chosen
}
pub(crate) fn imported_solid_names(count: usize, feature_name: &str) -> Vec<String> {
if count == 0 {
return Vec::new();
}
if count == 1 {
return vec![feature_name.to_string()];
}
let digits = count.to_string().len().max(2);
(1..=count)
.map(|index| format!("{feature_name}_SOLID_{index:0width$}", width = digits))
.collect()
}
pub fn native_import_payload(feature_name: &str, solids: &[BrepSolid]) -> Result<String, String> {
native_import_payload_with_appearance(feature_name, solids, &[])
}
pub fn native_import_payload_with_appearance(
feature_name: &str,
solids: &[BrepSolid],
appearances: &[BodyAppearance],
) -> Result<String, String> {
if solids.is_empty() {
return Err("import3d: native payload needs at least one solid".into());
}
let mut bodies = solids.to_vec();
let body_names = stamp_imported_names(&mut bodies, feature_name);
for solid in &mut bodies {
common::ensure_unique_face_names(solid);
common::stamp_derived_edge_names(solid);
}
for (index, (solid, body_name)) in bodies.iter().zip(&body_names).enumerate() {
if let Some(appearance) = appearances.get(index) {
stamp_appearance(body_name, &common::collect_face_names(solid), appearance);
}
}
let named: Vec<(&str, &BrepSolid)> = body_names
.iter()
.map(String::as_str)
.zip(bodies.iter())
.collect();
crate::snapshot_solids(&named)
}
pub fn context_applicable(_probe: &crate::feature_pipeline::SelectionProbe) -> bool {
false
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "IMPORT3D",
"shortName": "IMPORT3D",
"longName": "Import 3D Model",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the import feature"
}
}
})
}