diff --git a/Cargo.lock b/Cargo.lock index 94f7b77b5..047c97779 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4382,7 +4382,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/ramox81/cadcodec.git?rev=6c33e24#6c33e24dc3c72034afec8333f0554541f01cc508" +source = "git+https://github.com/ramox81/cadcodec.git?rev=58fac2c#58fac2c067cbd1346c08f371aaf92b0807a07f66" dependencies = [ "anyhow", "bitflags 2.13.1", diff --git a/Cargo.toml b/Cargo.toml index 03cabebf8..5223981c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -224,7 +224,7 @@ name = "integration" path = "tests/integration.rs" [patch."https://github.com/HakanSeven12/opencadcodec.git"] -opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "6c33e24" } +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "58fac2c" } [patch."https://github.com/HakanSeven12/opencadkernel.git"] opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "9490fb8" } diff --git a/src/io/iges_import.rs b/src/io/iges_import.rs index 64c0ee4d4..6317b4605 100644 --- a/src/io/iges_import.rs +++ b/src/io/iges_import.rs @@ -1,6 +1,8 @@ //! IGES reading: the manifold solids (type 186) of a file, their shells, -//! faces, loops and edges (514/510/508/504/502) over spline surfaces and -//! curves (128/126), lines and arcs (110/100), as kernel bodies. +//! faces, loops and edges (514/510/508/504/502), and its trimmed and bounded +//! surfaces (144/142 and 143/141), over spline surfaces and curves +//! (128/126), lines and arcs (110/100) and composite curves (102), as +//! kernel bodies. use std::collections::HashMap; @@ -80,6 +82,8 @@ struct Doc { entities: HashMap, /// File units to drawing units. scale: f64, + /// Points closer than this are one point, in drawing units. + resolution: f64, } /// A 3×4 matrix applied as `R·p + T`. @@ -205,6 +209,60 @@ impl Doc { }) } + /// A boundary curve as its pieces — each a curve and the points it runs + /// from and to — composite curves taken apart. + fn pieces(&self, pointer: f64) -> Option> { + let entity = self.get(pointer)?; + if entity.kind == 102 { + let count = entity.params.get(1).map_or(0.0, P::num) as usize; + let mut out = Vec::new(); + for k in 0..count { + out.extend(self.pieces(entity.params.get(2 + k)?.num())?); + } + return Some(out); + } + let curve = self.curve(pointer)?; + let v = |k: usize| entity.params.get(k).map_or(0.0, P::num); + let place = self.place(entity); + let (from, to) = match (&curve, entity.kind) { + (_, 110) => (place([v(1), v(2), v(3)]), place([v(4), v(5), v(6)])), + (_, 100) => (place([v(4), v(5), v(1)]), place([v(6), v(7), v(1)])), + (Curve3::Nurbs(nurbs), _) => { + let (t0, t1) = nurbs.domain(); + (nurbs.point_at_knot(t0), nurbs.point_at_knot(t1)) + } + _ => return None, + }; + Some(vec![(curve, from, to)]) + } + + /// The model-space pieces of a curve on a surface (142) or a boundary + /// (141), each of the latter's curves in the sense it is run. + fn boundary(&self, pointer: f64) -> Option> { + let entity = self.get(pointer)?; + let v = |k: usize| entity.params.get(k).map_or(0.0, P::num); + match entity.kind { + 142 => self.pieces(v(4)), + 141 => { + let mut out = Vec::new(); + let mut at = 5; + for _ in 0..v(4) as usize { + let mut pieces = self.pieces(v(at))?; + if v(at + 1) == 2.0 { + pieces.reverse(); + for piece in &mut pieces { + std::mem::swap(&mut piece.1, &mut piece.2); + } + } + out.extend(pieces); + at += 3 + v(at + 2) as usize; + } + Some(out) + } + _ => self.pieces(pointer), + } + } + fn surface(&self, pointer: f64) -> Option { let entity = self.get(pointer)?; if entity.kind != 128 { @@ -243,6 +301,12 @@ struct Assembly<'a> { build: Builder, vertices: HashMap<(usize, usize), VertexKey>, edges: HashMap<(usize, usize), EdgeKey>, + /// Vertices made where boundary curves meet, by position. + corners: Vec<([f64; 3], VertexKey)>, +} + +fn distance(a: [f64; 3], b: [f64; 3]) -> f64 { + (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::().sqrt() } impl Assembly<'_> { @@ -276,6 +340,119 @@ impl Assembly<'_> { Some(e) } + /// The vertex at a point, shared with one already there. + fn corner(&mut self, point: [f64; 3]) -> VertexKey { + let tolerance = self.doc.resolution; + if let Some((_, v)) = self.corners.iter().find(|(p, _)| distance(*p, point) <= tolerance) { + return *v; + } + let v = self.build.vertex(point); + self.corners.push((point, v)); + v + } + + /// A loop along curve pieces, in their order. + fn curve_ring(&mut self, pieces: Vec<(Curve3, [f64; 3], [f64; 3])>) -> Option> { + let mut ring = Vec::new(); + for (curve, from, to) in pieces { + if distance(from, to) <= self.doc.resolution && matches!(curve, Curve3::Line(_)) { + continue; + } + let (a, b) = (self.corner(from), self.corner(to)); + ring.push((self.build.edge(curve, a, b)?, true)); + } + (!ring.is_empty()).then_some(ring) + } + + /// A loop round the edge of a spline surface's parameter domain; sides + /// that close up to a point (a pole) are left out. + fn domain_ring(&mut self, surface: &NurbsSurface3) -> Option> { + let ((u0, u1), (v0, v1)) = surface.domain(); + let corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)]; + let sides = [(1, v0), (0, u1), (1, v1), (0, u0)]; + let mut pieces = Vec::new(); + for (k, (axis, at)) in sides.into_iter().enumerate() { + let (a, b) = (corners[k], corners[(k + 1) % 4]); + let (from, to) = (surface.point_at_knot(a.0, a.1), surface.point_at_knot(b.0, b.1)); + let middle = surface.point_at_knot(0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1)); + if distance(from, middle) <= self.doc.resolution && distance(to, middle) <= self.doc.resolution { + continue; + } + pieces.push((Curve3::Nurbs(surface.isocurve(axis, at)?), from, to)); + } + self.curve_ring(pieces) + } + + /// Turns a loop to run counter-clockwise round the surface's normal (an + /// outer one) or clockwise (a hole), judged by the area it encloses in + /// the surface's parameters. + fn orient(&self, surface: &Surface, ring: &mut [(EdgeKey, bool)], outer: bool) { + let mut uv = Vec::new(); + for (edge, forward) in ring.iter() { + let Some(edge) = self.build.body.edges.get(*edge) else { return }; + let Some(curve) = self.build.body.curves.get(edge.curve) else { return }; + let (t0, t1) = (edge.start_parameter, edge.end_parameter); + for k in 0..8 { + let s = k as f64 / 8.0; + let s = if *forward { s } else { 1.0 - s }; + if let Some(p) = surface.parameters_at(curve.point_at(t0 + (t1 - t0) * s)) { + uv.push(p); + } + } + } + let area: f64 = (0..uv.len()) + .map(|k| { + let (a, b) = (uv[k], uv[(k + 1) % uv.len()]); + a.0 * b.1 - b.0 * a.1 + }) + .sum(); + if (area > 0.0) != outer { + ring.reverse(); + for (_, forward) in ring.iter_mut() { + *forward = !*forward; + } + } + } + + /// A trimmed (144) or bounded (143) surface as a one-face sheet. + fn sheet(&mut self, entity: &Entity) -> Option<()> { + let v = |k: usize| entity.params.get(k).map_or(0.0, P::num); + let (surface_at, outer, holes): (f64, Option, Vec) = match entity.kind { + 144 => ( + v(1), + (v(2) != 0.0).then(|| v(4)), + (0..v(3) as usize).map(|k| v(5 + k)).collect(), + ), + 143 => { + let count = v(3) as usize; + (v(2), (count > 0).then(|| v(4)), (1..count).map(|k| v(4 + k)).collect()) + } + _ => return None, + }; + let Surface::Nurbs(nurbs) = self.doc.surface(surface_at)? else { return None }; + let surface = Surface::Nurbs(nurbs.clone()); + let mut first = match outer { + Some(boundary) => { + let pieces = self.doc.boundary(boundary)?; + self.curve_ring(pieces)? + } + None => self.domain_ring(&nurbs)?, + }; + self.orient(&surface, &mut first, true); + let mut loops = vec![first]; + for hole in holes { + let pieces = self.doc.boundary(hole)?; + let mut ring = self.curve_ring(pieces)?; + self.orient(&surface, &mut ring, false); + loops.push(ring); + } + self.build.lump(vec![vec![FaceSpec { + surface, + forward: true, + loops, + }]]) + } + fn ring(&mut self, pointer: f64) -> Option> { let entity = self.doc.get(pointer).filter(|e| e.kind == 508)?; let p = |k: usize| entity.params.get(k).map(P::num); @@ -330,6 +507,8 @@ impl Assembly<'_> { pub struct IgesModel { /// Each solid, scaled to the drawing, with its colour. pub bodies: Vec<(kernel::brep::Body, codec::types::Color)>, + /// Each trimmed or bounded surface, a one-face sheet, with its colour. + pub sheets: Vec<(kernel::brep::Body, codec::types::Color)>, } /// `drawing_mm`: millimetres per drawing unit, `None` for a unitless drawing. @@ -417,7 +596,12 @@ pub fn read(bytes: &[u8], drawing_mm: Option) -> Option { ); } let scale = drawing_mm.filter(|mm| *mm > 0.0).map_or(1.0, |mm| unit_mm / mm); - let doc = Doc { entities, scale }; + let resolution = globals.get(18).map(P::num).filter(|r| *r > 0.0).unwrap_or(1e-6) * scale; + let doc = Doc { + entities, + scale, + resolution, + }; let mut solids: Vec = doc .entities @@ -436,6 +620,7 @@ pub fn read(bytes: &[u8], drawing_mm: Option) -> Option { build: Builder::new(), vertices: HashMap::new(), edges: HashMap::new(), + corners: Vec::new(), }; let mut shells = Vec::new(); let outer = assembly.shell(shell, agrees != 0.0); @@ -450,5 +635,28 @@ pub fn read(bytes: &[u8], drawing_mm: Option) -> Option { } } } - Some(IgesModel { bodies }) + // The solids come in last first, as the reference reader places them. + bodies.reverse(); + let mut numbers: Vec = doc + .entities + .iter() + .filter(|(_, e)| matches!(e.kind, 143 | 144)) + .map(|(n, _)| *n) + .collect(); + numbers.sort_unstable(); + let mut sheets = Vec::new(); + for number in numbers { + let entity = &doc.entities[&number]; + let mut assembly = Assembly { + doc: &doc, + build: Builder::new(), + vertices: HashMap::new(), + edges: HashMap::new(), + corners: Vec::new(), + }; + if assembly.sheet(entity).is_some() { + sheets.push((assembly.build.body, doc.color(entity.color))); + } + } + Some(IgesModel { bodies, sheets }) } diff --git a/src/io/model_import.rs b/src/io/model_import.rs index a0d347057..f5515a0ae 100644 --- a/src/io/model_import.rs +++ b/src/io/model_import.rs @@ -5,7 +5,7 @@ use codec::entities::{Entity as _, Insert}; use codec::types::{Color, Matrix4, Transform, Vector3}; use codec::EntityType; -use crate::scene::convert::acis_import::{body_entity, sat_entities}; +use crate::scene::convert::acis_import::{painted_body_entity, sat_entities}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModelFormat { @@ -38,6 +38,55 @@ fn block_name(name: &str) -> String { if name.is_empty() { "Part".to_string() } else { name } } +/// A STEP presentation colour as the drawing colour it imports as: the +/// standard index colour it is close to — red, yellow, green, cyan, blue, +/// magenta, white, or the dark grey 250 (which black also becomes) — and +/// otherwise the true colour. +// ponytail: the closeness bands are fitted to measured imports (saturation +// at least 0.75, value at least 0.825 and hue within 7.5 degrees for the six +// hues; greys from 0.16 to 0.39 for 250 and from 0.935 for white); refit +// them if a colour on a band edge comes out differently. +pub fn step_colour(rgb: crate::io::step_import::Rgb) -> Color { + let [r, g, b] = rgb.map(|c| c.clamp(0.0, 1.0)); + let true_colour = Color::Rgb { + r: (r * 255.0).round() as u8, + g: (g * 255.0).round() as u8, + b: (b * 255.0).round() as u8, + }; + let max = r.max(g).max(b); + let min = r.min(g).min(b); + if max == 0.0 { + return Color::Index(250); + } + let saturation = (max - min) / max; + if saturation <= 0.1 { + return if max >= 0.935 { + Color::Index(7) + } else if (0.16..0.395).contains(&max) { + Color::Index(250) + } else { + true_colour + }; + } + if saturation < 0.75 || max < 0.825 { + return true_colour; + } + let delta = max - min; + let hue = if max == r { + 60.0 * ((g - b) / delta).rem_euclid(6.0) + } else if max == g { + 60.0 * ((b - r) / delta + 2.0) + } else { + 60.0 * ((r - g) / delta + 4.0) + }; + let sector = (hue / 60.0).round(); + if (hue - 60.0 * sector).abs() > 7.5 { + return true_colour; + } + // Red, yellow, green, cyan, blue, magenta. + Color::Index([1, 2, 3, 4, 5, 6][sector as usize % 6]) +} + /// An insert of `block` placed by `placement` (columns: axes, origin). fn placed_insert(block: &str, placement: &[[f64; 4]; 3]) -> EntityType { let mut insert = Insert::new(block, Vector3::ZERO); @@ -89,10 +138,9 @@ fn step_blocks(model: crate::io::step_import::StepModel, scale: f64) -> Imported let mut entities: Vec = product .bodies .iter() - .filter_map(body_entity) - .map(|mut entity| { - entity.common_mut().color = Color::ByBlock; - entity + .filter_map(|body| { + let faces: Vec> = body.faces.iter().map(|c| c.map(step_colour)).collect(); + painted_body_entity(&body.body, body.colour.map(step_colour), &faces, false) }) .collect(); entities.extend( @@ -131,18 +179,20 @@ pub fn read(format: ModelFormat, bytes: &[u8], drawing_mm: Option) -> Optio let scale = ratio(model.unit_mm, drawing_mm); step_blocks(model, scale) } - ModelFormat::Iges => Imported { - blocks: Vec::new(), - entities: crate::io::iges_import::read(bytes, drawing_mm)? - .bodies - .iter() - .filter_map(|(body, color)| { - let mut entity = body_entity(body)?; - entity.common_mut().color = *color; - Some(entity) - }) - .collect(), - }, + ModelFormat::Iges => { + // Solids, then the surfaces as surfaces however flat; faces and + // edges carry the object's colour. + let model = crate::io::iges_import::read(bytes, drawing_mm)?; + let solids = model.bodies.iter().map(|body| (body, false)); + let sheets = model.sheets.iter().map(|body| (body, true)); + Imported { + blocks: Vec::new(), + entities: solids + .chain(sheets) + .filter_map(|((body, color), sheet)| painted_body_entity(body, Some(*color), &[], sheet)) + .collect(), + } + } }; let solids = imported .blocks diff --git a/src/io/step_import.rs b/src/io/step_import.rs index c4b951dda..4953d7052 100644 --- a/src/io/step_import.rs +++ b/src/io/step_import.rs @@ -197,9 +197,68 @@ struct Doc { instances: HashMap, /// Plane-angle unit in radians. angle: f64, + /// The colour each styled item (a solid, a shell or a face) is shown + /// in, as 0–1 RGB. + styles: HashMap, } +/// An RGB colour as 0–1 values. +pub type Rgb = [f64; 3]; + impl Doc { + /// A presentation colour: an RGB one or a predefined one. + fn colour(&self, p: &P) -> Option { + let (kind, params) = self.simple(p.reference()?)?; + match kind { + "COLOUR_RGB" => Some([params.get(1)?.num()?, params.get(2)?.num()?, params.get(3)?.num()?]), + "DRAUGHTING_PRE_DEFINED_COLOUR" => Some(match params.first()?.text()? { + "red" => [1.0, 0.0, 0.0], + "green" => [0.0, 1.0, 0.0], + "blue" => [0.0, 0.0, 1.0], + "yellow" => [1.0, 1.0, 0.0], + "magenta" => [1.0, 0.0, 1.0], + "cyan" => [0.0, 1.0, 1.0], + "black" => [0.0, 0.0, 0.0], + "white" => [1.0, 1.0, 1.0], + _ => return None, + }), + _ => None, + } + } + + /// The colour a styled item's presentation styles give it: its surface + /// fill (or rendering) colour, or failing that its curve colour. + fn style_colour(&self, styles: &P) -> Option { + let mut stack: Vec<&P> = vec![styles]; + let mut seen = Vec::new(); + let mut curve = None; + while let Some(p) = stack.pop() { + match p { + P::List(items) | P::Typed(items) => stack.extend(items.iter().rev()), + P::Ref(id) if !seen.contains(id) => { + seen.push(*id); + let Some((kind, params)) = self.simple(*id) else { continue }; + match kind { + "FILL_AREA_STYLE_COLOUR" => { + if let Some(c) = params.get(1).and_then(|c| self.colour(c)) { + return Some(c); + } + } + "SURFACE_STYLE_RENDERING" | "SURFACE_STYLE_RENDERING_WITH_PROPERTIES" => { + if let Some(c) = params.get(1).and_then(|c| self.colour(c)) { + return Some(c); + } + } + "CURVE_STYLE" => curve = curve.or_else(|| params.get(3).and_then(|c| self.colour(c))), + _ => stack.extend(params.iter().rev()), + } + } + _ => {} + } + } + curve + } + fn parts(&self, id: u64) -> Option<&Instance> { self.instances.get(&id) } @@ -392,6 +451,8 @@ struct Assembly<'a> { vertices: HashMap, /// Each edge, and whether it runs from the file's edge end to its start. edges: HashMap, + /// The file's face of each face built, in build order. + faces: Vec, } impl Assembly<'_> { @@ -450,6 +511,7 @@ impl Assembly<'_> { fn face(&mut self, p: &P, flip: bool) -> Option { let id = p.reference()?; + self.faces.push(id); let (_, params) = self.doc.simple(id)?; let surface = self.doc.surface(params.get(2)?)?; let mut outer = Vec::new(); @@ -505,10 +567,19 @@ pub type Placement = [[f64; 4]; 3]; const IDENTITY: Placement = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]]; +/// A body with the colours the file shows it in. +pub struct StepBody { + pub body: kernel::brep::Body, + /// The colour of the solid (or shell model) as a whole. + pub colour: Option, + /// Each face's own colour, in the body's face order. + pub faces: Vec>, +} + /// One product: its own bodies and where each of its components sits. pub struct StepProduct { pub name: String, - pub bodies: Vec, + pub bodies: Vec, /// Components: the product (an index into [`StepModel::products`]) and /// its placement in this one. pub children: Vec<(usize, Placement)>, @@ -549,7 +620,7 @@ fn unit_size(doc: &Doc, id: u64) -> Option { /// The bodies of one B-rep item: a solid, a solid with voids, or each shell /// of a shell model. -fn item_bodies(doc: &Doc, id: u64) -> Vec { +fn item_bodies(doc: &Doc, id: u64) -> Vec { let Some((kind, params)) = doc.simple(id) else { return Vec::new(); }; @@ -576,11 +647,22 @@ fn item_bodies(doc: &Doc, id: u64) -> Vec { build: Builder::new(), vertices: HashMap::new(), edges: HashMap::new(), + faces: Vec::new(), }; let faces: Option> = lump.iter().map(|shell| assembly.shell(shell)).collect(); if let Some(faces) = faces { if assembly.build.lump(faces).is_some() { - bodies.push(assembly.build.body); + // The item's own style, or its (first) shell's. + let colour = doc.styles.get(&id).copied().or_else(|| { + lump.iter() + .filter_map(|shell| shell.reference()) + .find_map(|shell| doc.styles.get(&shell).copied()) + }); + bodies.push(StepBody { + body: assembly.build.body, + colour, + faces: assembly.faces.iter().map(|face| doc.styles.get(face).copied()).collect(), + }); } } } @@ -648,6 +730,7 @@ pub fn read(bytes: &[u8]) -> Option { let mut doc = Doc { instances, angle: 1.0, + styles: HashMap::new(), }; let mut unit_mm = 1.0; @@ -669,6 +752,23 @@ pub fn read(bytes: &[u8]) -> Option { break; } doc.angle = angle; + // Styled items: plain ones first, so an overriding style wins. + let mut styled = Vec::new(); + for kind in ["STYLED_ITEM", "OVER_RIDING_STYLED_ITEM"] { + for &id in &ids { + if doc.kind(id) != kind { + continue; + } + let Some((_, params)) = doc.simple(id) else { continue }; + let (Some(styles), Some(item)) = (params.get(1), params.get(2).and_then(P::reference)) else { + continue; + }; + if let Some(colour) = doc.style_colour(styles) { + styled.push((item, colour)); + } + } + } + doc.styles.extend(styled); let doc = doc; // What each instance id is, by kind. diff --git a/src/scene/convert/acis_import.rs b/src/scene/convert/acis_import.rs index 57da76578..f7485b328 100644 --- a/src/scene/convert/acis_import.rs +++ b/src/scene/convert/acis_import.rs @@ -42,7 +42,11 @@ fn class(document: &SatDocument) -> Option { /// One body's document as its drawing object. fn entity(document: &SatDocument) -> Option { - Some(match class(document)? { + entity_as(document, class(document)?) +} + +fn entity_as(document: &SatDocument, class: Class) -> Option { + Some(match class { Class::Solid => { let mut solid = Solid3D::new(); solid.set_sat_document(document); @@ -68,8 +72,93 @@ pub fn sat_entities(text: &str) -> Option> { Some(document.split_bodies().iter().filter_map(entity).collect()) } -/// A kernel body as its drawing object, written as exact ACIS. -pub fn body_entity(body: &kernel::brep::Body) -> Option { - let document = super::acis_export::solid_to_sat(body)?; - entity(&document) +/// 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, + } +} + +/// 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 { + let packed = (0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b); + chain.push(("truecolor-adesk-attrib", i64::from(packed as i32))); + } + 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 them too, as the reference modeler stores them. A `sheet` is a +/// SURFACE however flat it is. +pub fn painted_body_entity( + body: &kernel::brep::Body, + colour: Option, + faces: &[Option], + sheet: bool, +) -> Option { + let mut document = super::acis_export::solid_to_sat(body)?; + let base = colour.unwrap_or(codec::types::Color::ByBlock); + let own: std::collections::HashMap = 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 = (0..document.record_count()) + .filter(|&at| document.record(at).is_some_and(|r| r.entity_type == "face")) + .collect(); + let edge_records: Vec = (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) } diff --git a/src/scene/convert/acis_kernel.rs b/src/scene/convert/acis_kernel.rs index c37992c28..0ec93b26a 100644 --- a/src/scene/convert/acis_kernel.rs +++ b/src/scene/convert/acis_kernel.rs @@ -70,13 +70,24 @@ pub fn tessellate_sat( (owner >= 0 && handle > 0).then(|| (owner, codec::Handle::new(handle as u64))) }) .collect(); + // A true colour attribute outranks the index one beside it, which only + // holds its nearest index. let face_colors: std::collections::HashMap = document .records .iter() .filter(|record| record.entity_type == "color-adesk-attrib") + .chain( + document + .records + .iter() + .filter(|record| record.entity_type == "truecolor-adesk-attrib"), + ) .filter_map(|record| { let owner = record.token_pointer(2)?.0; - let value = record.token(3)?.as_integer()?; + let mut value = record.token(3)?.as_integer()?; + if record.entity_type == "truecolor-adesk-attrib" { + value = (value & 0x00FF_FFFF) | 0x0100_0000; + } let source = if (1..=255).contains(&value) { codec::Color::from_index(value as i16) } else if value > 257 {