From 8e9bd7db6901a24d93563965af247d1c5ccb4661 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:12:46 +0300 Subject: [PATCH] IMPORT: STEP colours and IGES trimmed and bounded surfaces STEP presentation styles colour what they style: a solid in the standard index colour its RGB is close to, else in the true colour, and a face with a style of its own (with the edges round it) through the face and edge colour attributes. IGES trimmed (144) and bounded (143) surfaces come in as one SURFACE each in the entry's colour, after the solids, which now come in last first. Faces show a true colour attribute in that colour. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/io/iges_import.rs | 216 ++++++++++++++++++++++++++++++- src/io/model_import.rs | 84 +++++++++--- src/io/step_import.rs | 106 ++++++++++++++- src/scene/convert/acis_import.rs | 99 +++++++++++++- src/scene/convert/acis_kernel.rs | 13 +- 7 files changed, 490 insertions(+), 32 deletions(-) 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 {