forked from 1b-agent/opencadstudio-upstream
STEP assemblies: a link that closes a loop in the assembly is dropped, so no block inserts itself; the walk is iterative. Each imported block gets a name the drawing does not use yet instead of silently reusing an existing definition, inserts are pointed at the names the blocks got, and an insert of a block that could not be defined is left out with the error reported. Names that come from files (STEP products, 3D Studio objects, materials and cameras, WMF font faces) go through a new shared scene::sanitized_table_name, which applies the block-name rules (reserved and control characters, 255 characters). 3D Studio: meshes with more vertices than a polyface mesh can index are split over several meshes. Single-layer mode uses the current layer. Materials are only created when the drawing has a material dictionary. Camera views get the current view's proportions and the perspective flag. Messages show paths as given, the file-not-found and read-failure messages are plain text of our own, and the "Press Esc" line is gone since the import cannot be cancelled. The true-colour packing is shared. "Select ACIS File" and "Import WMF" get catalog entries and keys in all 22 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
6.1 KiB
Rust
169 lines
6.1 KiB
Rust
//! ACIS text (SAT) files and kernel bodies as drawing objects.
|
|
//!
|
|
//! Each body of a file becomes one object: a closed solid a 3DSOLID, a sheet
|
|
//! whose faces all lie on planes a REGION, any other sheet a SURFACE.
|
|
|
|
use codec::entities::acis::{SatDocument, SatFace, Sidedness};
|
|
use codec::entities::{AcisData, Region, Solid3D, Surface, SurfaceKind};
|
|
use codec::EntityType;
|
|
|
|
/// What a body restores as.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum Class {
|
|
Solid,
|
|
Region,
|
|
Surface,
|
|
}
|
|
|
|
fn class(document: &SatDocument) -> Option<Class> {
|
|
let faces: Vec<SatFace<'_>> = document
|
|
.records
|
|
.iter()
|
|
.filter_map(SatFace::from_record)
|
|
.collect();
|
|
if faces.is_empty() {
|
|
return None;
|
|
}
|
|
if faces
|
|
.iter()
|
|
.all(|face| face.sidedness() != Sidedness::Double)
|
|
{
|
|
return Some(Class::Solid);
|
|
}
|
|
let planar = faces.iter().all(|face| {
|
|
face.surface()
|
|
.index()
|
|
.and_then(|at| document.record(at))
|
|
.is_some_and(|surface| surface.entity_type == "plane-surface")
|
|
});
|
|
Some(if planar { Class::Region } else { Class::Surface })
|
|
}
|
|
|
|
/// One body's document as its drawing object.
|
|
fn entity(document: &SatDocument) -> Option<EntityType> {
|
|
entity_as(document, class(document)?)
|
|
}
|
|
|
|
fn entity_as(document: &SatDocument, class: Class) -> Option<EntityType> {
|
|
Some(match class {
|
|
Class::Solid => {
|
|
let mut solid = Solid3D::new();
|
|
solid.set_sat_document(document);
|
|
EntityType::Solid3D(solid)
|
|
}
|
|
Class::Region => {
|
|
let mut region = Region::new();
|
|
region.set_sat_document(document);
|
|
EntityType::Region(region)
|
|
}
|
|
Class::Surface => {
|
|
let mut surface = Surface::new(SurfaceKind::Generic);
|
|
surface.acis_data = AcisData::from_sat(&document.to_sat_string());
|
|
EntityType::Surface(Box::new(surface))
|
|
}
|
|
})
|
|
}
|
|
|
|
/// The objects of an ACIS text file, one per body with faces; `None` when
|
|
/// the text is no ACIS data.
|
|
pub fn sat_entities(text: &str) -> Option<Vec<EntityType>> {
|
|
let document = SatDocument::parse(text).ok()?;
|
|
Some(document.split_bodies().iter().filter_map(entity).collect())
|
|
}
|
|
|
|
/// The value of an ACIS colour attribute: the index (0 by block), or the
|
|
/// index nearest a true colour.
|
|
fn attribute_index(color: codec::types::Color) -> i64 {
|
|
match color {
|
|
codec::types::Color::Index(index) => i64::from(index),
|
|
codec::types::Color::Rgb { r, g, b } => i64::from(codec::types::nearest_aci(r, g, b)),
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// A true colour packed as a 32-bit colour value: the true-colour flag
|
|
/// byte, then red, green and blue.
|
|
pub(crate) fn packed_true_colour(r: u8, g: u8, b: u8) -> i32 {
|
|
((0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b)) as i32
|
|
}
|
|
|
|
/// Hangs colour attributes on a record: a true colour first, then its
|
|
/// index, as imported models carry them.
|
|
fn paint_record(document: &mut codec::entities::acis::SatDocument, owner: usize, color: codec::types::Color) {
|
|
use codec::entities::acis::{SatPointer, SatRecord, SatToken};
|
|
let mut chain: Vec<(&str, i64)> = Vec::new();
|
|
if let codec::types::Color::Rgb { r, g, b } = color {
|
|
chain.push(("truecolor-adesk-attrib", i64::from(packed_true_colour(r, g, b))));
|
|
}
|
|
chain.push(("color-adesk-attrib", attribute_index(color)));
|
|
let Some(existing) = document.record(owner).map(|record| record.attribute) else {
|
|
return;
|
|
};
|
|
let first = document.record_count() as i32;
|
|
let count = chain.len() as i32;
|
|
for (k, (kind, value)) in chain.into_iter().enumerate() {
|
|
let at = first + k as i32;
|
|
let next = if k as i32 + 1 < count { SatPointer::new(at + 1) } else { existing };
|
|
let previous = if k == 0 { SatPointer::NULL } else { SatPointer::new(at - 1) };
|
|
let mut record = SatRecord::new(-1, kind);
|
|
record.tokens = vec![
|
|
SatToken::Pointer(next),
|
|
SatToken::Pointer(previous),
|
|
SatToken::Pointer(SatPointer::new(owner as i32)),
|
|
SatToken::Integer(value),
|
|
];
|
|
document.add_record(record);
|
|
}
|
|
if let Some(record) = document.record_mut(owner) {
|
|
record.attribute = SatPointer::new(first);
|
|
}
|
|
}
|
|
|
|
/// A kernel body as its drawing object in `colour` (by block when `None`),
|
|
/// with the faces given a colour of their own carrying it, and the edges
|
|
/// bounding those faces carrying it too. A `sheet` is a SURFACE however
|
|
/// flat it is.
|
|
pub fn painted_body_entity(
|
|
body: &kernel::brep::Body,
|
|
colour: Option<codec::types::Color>,
|
|
faces: &[Option<codec::types::Color>],
|
|
sheet: bool,
|
|
) -> Option<EntityType> {
|
|
let mut document = super::acis_export::solid_to_sat(body)?;
|
|
let base = colour.unwrap_or(codec::types::Color::ByBlock);
|
|
let own: std::collections::HashMap<kernel::brep::FaceKey, codec::types::Color> = body
|
|
.faces
|
|
.keys()
|
|
.zip(faces)
|
|
.filter_map(|(key, color)| Some((key, (*color)?)))
|
|
.collect();
|
|
// The writer puts faces and edges in the body's own order.
|
|
let face_records: Vec<usize> = (0..document.record_count())
|
|
.filter(|&at| document.record(at).is_some_and(|r| r.entity_type == "face"))
|
|
.collect();
|
|
let edge_records: Vec<usize> = (0..document.record_count())
|
|
.filter(|&at| document.record(at).is_some_and(|r| r.entity_type == "edge"))
|
|
.collect();
|
|
for (key, at) in body.faces.keys().zip(face_records) {
|
|
if let Some(color) = own.get(&key) {
|
|
paint_record(&mut document, at, *color);
|
|
}
|
|
}
|
|
for ((_, edge), at) in body.edges.iter().zip(edge_records) {
|
|
let bounding = edge.coedges.iter().find_map(|coedge| {
|
|
let ring = body.coedges.get(*coedge)?.owner;
|
|
own.get(&body.loops.get(ring)?.owner).copied()
|
|
});
|
|
if let Some(color) = bounding {
|
|
paint_record(&mut document, at, color);
|
|
}
|
|
}
|
|
let mut entity = if sheet {
|
|
entity_as(&document, Class::Surface)?
|
|
} else {
|
|
entity(&document)?
|
|
};
|
|
entity.common_mut().color = base;
|
|
Some(entity)
|
|
}
|