From c571b1346936bbe75876818cbc2f7253f92b8377 Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 10:56:03 -0400 Subject: [PATCH] M12: PRC reader: product tree and styles (#1297) Walk the PRC product tree into placed, coloured tessellations. - PrcFile::placements / placements_with(StyleAlpha, EntityOverrides): every drawable tessellation reached from the model file's roots, placed by its occurrence path's locations and then the representation item's local coordinate systems (a 4x4 Matrix) [WD 7.3.10.1, 7.6.3.2]. - Colours: styles and materials from the file structure globals, the father/son inheritance bits [WD 7.2.4.2], style transparency combined with material alpha by StyleAlpha, per-face styles and topology references via Placement::triangle_colours. A prototype's part is coloured by its own structure's styles. A colour index counts the colour array's doubles (pdf-issues #816). - Occurrences' entity references: colour, hide or relocate the entity they name, in the occurrence's subtree, everywhere, or not at all. - PrcFile::model_tree: the assembly tree as a model-tree panel lists it, with name sources, visibility and each node's placement range. - TriangleMesh::transformed applies a Matrix: positions, cofactor-mapped normals, winding flipped under a mirror. Never panics: occurrence and prototype cycles are Malformed; depth 64, 2^20 visits, MAX_PLACEMENTS and a face-override ceiling bound the walk; counts are checked against the remaining data before allocating. Markups, views, scene lights and clipping planes are Unsupported; textures are read past. Field orders the working draft leaves open follow prc-rs (MIT); each is marked `// prc-rs:`, as are the step-1 container, Double and schema choices that did. Adapted from pdfcer (MIT, same author), crates/pdfcer-3d/src/tree and vec3.rs at 0c9742b5936232af89cd707138ef149dc1f4a660. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/prc/README.md | 13 +- crates/prc/src/bits.rs | 2 + crates/prc/src/container.rs | 3 +- crates/prc/src/entity.rs | 7 +- crates/prc/src/geometry.rs | 179 ++++++++++ crates/prc/src/lib.rs | 8 +- crates/prc/src/schema.rs | 2 + crates/prc/src/tess.rs | 2 +- crates/prc/src/tess/tests.rs | 7 + crates/prc/src/tree/mod.rs | 293 ++++++++++++++++ crates/prc/src/tree/node.rs | 68 ++++ crates/prc/src/tree/overrides.rs | 128 +++++++ crates/prc/src/tree/read.rs | 451 ++++++++++++++++++++++++ crates/prc/src/tree/style.rs | 348 +++++++++++++++++++ crates/prc/src/tree/tests.rs | 576 +++++++++++++++++++++++++++++++ crates/prc/src/tree/walk.rs | 262 ++++++++++++++ 16 files changed, 2344 insertions(+), 5 deletions(-) create mode 100644 crates/prc/src/geometry.rs create mode 100644 crates/prc/src/tree/mod.rs create mode 100644 crates/prc/src/tree/node.rs create mode 100644 crates/prc/src/tree/overrides.rs create mode 100644 crates/prc/src/tree/read.rs create mode 100644 crates/prc/src/tree/style.rs create mode 100644 crates/prc/src/tree/tests.rs create mode 100644 crates/prc/src/tree/walk.rs diff --git a/crates/prc/README.md b/crates/prc/README.md index cb3865d3fd..614231cf64 100644 --- a/crates/prc/README.md +++ b/crates/prc/README.md @@ -14,6 +14,10 @@ PrcFile::parse_with_limit(data, max_inflated) -> Result FileStructure::section(SectionKind) -> &[u8] // inflated globals / tree / tessellation / geometry / extra geometry FileStructure::schema() -> Result // the globals section's leading schema FileStructure::tessellations() -> Result, PrcError> // meshes, wires, markups, compressed meshes +PrcFile::placements() / placements_with(StyleAlpha, EntityOverrides) -> Result, PrcError> +PrcFile::model_tree() -> Result, PrcError> // the assembly tree as a model-tree panel lists it +Placement::triangle_colours(&TriangleMesh) -> Option>> +TriangleMesh::transformed(&Matrix) -> TriangleMesh Schema::read(&mut BitReader) / Schema::skip_added_fields(entity_type, &mut BitReader) BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_vbn, nbits_then_unsigned, string, float_as_bytes, double ``` @@ -26,6 +30,13 @@ BitReader: bit, bits, character, unsigned_integer, integer, integer_vbn, double_ `Wire` is polylines; `Markup` is read past. - `Tessellation::Compressed` is read to its end so later entities decode; its triangles are not rebuilt yet. - A face may carry at most 8 texture-coordinate sets; more is `Malformed`. Optimised vertex colours are `Unsupported`. -- Not yet in the crate: the product tree and rebuilding compressed meshes. +- The product tree: each drawable tessellation placed by its occurrence path's locations and the item's coordinate + systems (a 4x4 `Matrix`), coloured by the style inheritance rules, with occurrences' entity references applied + (`EntityOverrides`: subtree, everywhere or ignored) and style transparency combined with material alpha by `StyleAlpha`. + A prototype's part is coloured by its own file structure's styles. Each `ModelNode` lists its name, where the name + came from, its visibility and the range of placements it drew. +- Occurrence and prototype cycles are `Malformed`; the walk is capped at depth 64, 2^20 visits and `MAX_PLACEMENTS`. +- Trees carrying markups (PMI), views, scene lights or clipping planes are `Unsupported`; textures are read past. +- Not yet in the crate: rebuilding compressed meshes, textures. - The `Double` code table's rows come from [prc-rs](https://github.com/ralovich/prc-rs) (MIT); its notice is in `src/acof/table.rs`. A few reading choices also follow prc-rs; each is marked `// prc-rs:` in the source. diff --git a/crates/prc/src/bits.rs b/crates/prc/src/bits.rs index b4a2ff352b..a1efe7a64f 100644 --- a/crates/prc/src/bits.rs +++ b/crates/prc/src/bits.rs @@ -201,6 +201,8 @@ impl<'a> BitReader<'a> { /// [`PrcError::UnknownDoubleCode`], [`PrcError::Truncated`], or [`PrcError::Malformed`] /// for a back-reference outside the value. pub fn double(&mut self) -> Result { + // prc-rs: the decoding steps (code lookup, sign, exponent, byte-wise mantissa with + // back-references) follow prc-rs's. let mut code = 0u32; let mut entry = None; for n in 1..=acof::MAX_CODE_BITS { diff --git a/crates/prc/src/container.rs b/crates/prc/src/container.rs index 8a44921334..15c70af8fc 100644 --- a/crates/prc/src/container.rs +++ b/crates/prc/src/container.rs @@ -181,7 +181,7 @@ impl<'a> Bytes<'a> { /// A stored `u32` offset or count as a `usize` (saturating on a 16-bit target, where no data /// that large could exist). -fn offset(v: u32) -> usize { +pub(crate) fn offset(v: u32) -> usize { usize::try_from(v).unwrap_or(usize::MAX) } @@ -316,6 +316,7 @@ impl PrcFile { } let mut budget = Budget { left: limit, limit }; + // prc-rs: the last section runs to the model file's start. let last = mf_start.min(data.len()); let file_structures = descriptions.iter().map(|(id, offs)| file_structure(data, *id, &Extent { offs, last }, &mut budget)).collect::, _>>()?; diff --git a/crates/prc/src/entity.rs b/crates/prc/src/entity.rs index e6f488c8f8..1ad53dbd72 100644 --- a/crates/prc/src/entity.rs +++ b/crates/prc/src/entity.rs @@ -4,6 +4,7 @@ use crate::PrcError; use crate::bits::BitReader; use crate::schema::Schema; +use crate::tree::Graphics; const ATTRIBUTE: u32 = 201; @@ -21,11 +22,15 @@ pub(crate) struct Ctx<'a, 's> { /// The current name: what a `same_name` entity reuses; every entity's own name replaces /// it [WD 7.2.3.4]. Each section starts with none. pub(crate) name: Option, + /// The PRC unique id of the last `ContentPRCRefBase` read [WD 7.2.3.2]. + pub(crate) uid: u32, + /// The current graphics: what a `same_graphics` entity reuses [WD 7.2.4]. + pub(crate) graphics: Graphics, } impl<'a, 's> Ctx<'a, 's> { pub(crate) fn new(r: BitReader<'a>, schema: &'s Schema, version: u32) -> Self { - Ctx { r, schema, version, name: None } + Ctx { r, schema, version, name: None, uid: 0, graphics: Graphics::default() } } /// Reads an entity type code; any value but `want` is malformed. diff --git a/crates/prc/src/geometry.rs b/crates/prc/src/geometry.rs new file mode 100644 index 0000000000..3328bf10f1 --- /dev/null +++ b/crates/prc/src/geometry.rs @@ -0,0 +1,179 @@ +//! Model-space vectors and matrices, and their application to meshes. + +use crate::TriangleMesh; + +/// A point or direction in model space. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub(crate) struct Vec3(pub(crate) [f64; 3]); + +impl Vec3 { + /// The right-handed cross product `self × b`. + pub(crate) fn cross(self, b: Vec3) -> Vec3 { + let ([ax, ay, az], [bx, by, bz]) = (self.0, b.0); + Vec3([ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx]) + } +} + +/// A 4×4 matrix, `m.0[row][col]`, acting on column vectors. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Matrix(pub [[f64; 4]; 4]); + +impl Default for Matrix { + fn default() -> Self { + Matrix::IDENTITY + } +} + +impl Matrix { + /// The identity matrix. + pub const IDENTITY: Matrix = Matrix([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]); + + /// The entry at `row`, `col`; 0 outside the matrix. + #[must_use] + pub fn at(&self, row: usize, col: usize) -> f64 { + self.0.get(row).and_then(|r| r.get(col)).copied().unwrap_or(0.0) + } + + /// `self × b`: `b` applied first. + #[must_use] + pub fn multiply(&self, b: &Matrix) -> Matrix { + let mut m = [[0.0; 4]; 4]; + for (i, row) in m.iter_mut().enumerate() { + for (j, cell) in row.iter_mut().enumerate() { + *cell = (0..4).map(|k| self.at(i, k) * b.at(k, j)).sum(); + } + } + Matrix(m) + } + + /// `self × (p, 1)`, divided by the homogeneous coordinate when it is neither 0 nor 1. + #[must_use] + pub fn transform_point(&self, p: [f64; 3]) -> [f64; 3] { + let [x, y, z] = p; + let row = |i: usize| self.at(i, 0) * x + self.at(i, 1) * y + self.at(i, 2) * z + self.at(i, 3); + let w = row(3); + let s = if w == 0.0 || w == 1.0 { 1.0 } else { 1.0 / w }; + [row(0) * s, row(1) * s, row(2) * s] + } + + /// The determinant of the upper-left 3×3 block. + fn det3(&self) -> f64 { + let a = |i, j| self.at(i, j); + a(0, 0) * (a(1, 1) * a(2, 2) - a(1, 2) * a(2, 1)) - a(0, 1) * (a(1, 0) * a(2, 2) - a(1, 2) * a(2, 0)) + + a(0, 2) * (a(1, 0) * a(2, 1) - a(1, 1) * a(2, 0)) + } + + /// Cofactor `(i, j)` of the upper-left 3×3 block (`i`, `j` < 3). + fn cofactor(&self, i: usize, j: usize) -> f64 { + let (r0, r1) = ((i + 1) % 3, (i + 2) % 3); + let (c0, c1) = ((j + 1) % 3, (j + 2) % 3); + self.at(r0, c0) * self.at(r1, c1) - self.at(r0, c1) * self.at(r1, c0) + } +} + +impl TriangleMesh { + /// This mesh with every position mapped through `m` and every stored normal through its + /// inverse transpose (renormalised); a mirroring `m` (negative determinant) reverses each + /// triangle so the outside stays counter-clockwise. + /// + /// ``` + /// use pdfcraft_prc::{Matrix, TriangleMesh}; + /// let mut mirror = Matrix::IDENTITY; + /// mirror.0[0][0] = -1.0; + /// let mut mesh = TriangleMesh::default(); + /// mesh.positions = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; + /// mesh.triangles = vec![[0, 1, 2]]; + /// let placed = mesh.transformed(&mirror); + /// assert_eq!(placed.positions, [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]); + /// assert_eq!(placed.triangles, [[0, 2, 1]]); + /// ``` + #[must_use] + pub fn transformed(&self, m: &Matrix) -> Self { + let mut out = self.clone(); + for p in &mut out.positions { + *p = m.transform_point(*p); + } + // The cofactor matrix is det times the inverse transpose, so its sign is corrected + // and the result renormalised. + let det = m.det3(); + let sign = if det < 0.0 { -1.0 } else { 1.0 }; + for n in &mut out.normals { + let [x, y, z] = *n; + let v = [0, 1, 2].map(|i| sign * (m.cofactor(i, 0) * x + m.cofactor(i, 1) * y + m.cofactor(i, 2) * z)); + let len = v.iter().map(|c| c * c).sum::().sqrt(); + *n = if len > 0.0 && len.is_finite() { v.map(|c| c / len) } else { v }; + } + if det < 0.0 { + for t in &mut out.triangles { + t.swap(1, 2); + } + for t in &mut out.triangle_normals { + t.swap(1, 2); + } + for t in out.triangle_uvs.iter_mut().flatten().flatten() { + t.swap(1, 2); + } + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cross_follows_the_right_hand_rule() { + assert_eq!(Vec3([1.0, 0.0, 0.0]).cross(Vec3([0.0, 1.0, 0.0])), Vec3([0.0, 0.0, 1.0])); + assert_eq!(Vec3([0.0, 1.0, 0.0]).cross(Vec3([1.0, 0.0, 0.0])), Vec3([0.0, 0.0, -1.0])); + } + + #[test] + fn multiply_applies_the_right_operand_first() { + let mut t = Matrix::IDENTITY; + t.0[0][3] = 5.0; + let mut s = Matrix::IDENTITY; + s.0[0][0] = 2.0; + assert_eq!(t.multiply(&s).transform_point([1.0, 0.0, 0.0]), [7.0, 0.0, 0.0]); + assert_eq!(s.multiply(&t).transform_point([1.0, 0.0, 0.0]), [12.0, 0.0, 0.0]); + } + + #[test] + fn a_projective_point_is_divided_by_w() { + let mut m = Matrix::IDENTITY; + m.0[3][3] = 2.0; + assert_eq!(m.transform_point([2.0, 4.0, 6.0]), [1.0, 2.0, 3.0]); + m.0[3][3] = 0.0; + assert_eq!(m.transform_point([2.0, 4.0, 6.0]), [2.0, 4.0, 6.0]); + } + + #[test] + fn normals_take_the_inverse_transpose_and_mirror_with_their_triangle() { + // A shear in x by y turns the plane x = 0 into x = y; its x normal tilts to stay perpendicular. + let mut shear = Matrix::IDENTITY; + shear.0[0][1] = 1.0; + let mesh = TriangleMesh { + positions: vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]], + triangles: vec![[0, 1, 2]], + normals: vec![[1.0, 0.0, 0.0]], + triangle_normals: vec![[0, 0, 0]], + ..TriangleMesh::default() + }; + let n = shear.transformed_normal(&mesh); + let s = std::f64::consts::FRAC_1_SQRT_2; + assert!((n[0] - s).abs() < 1e-12 && (n[1] + s).abs() < 1e-12 && n[2] == 0.0, "{n:?}"); + + let mut mirror = Matrix::IDENTITY; + mirror.0[2][2] = -1.0; + let placed = mesh.transformed(&mirror); + assert_eq!(placed.triangles, [[0, 2, 1]]); + assert_eq!(placed.triangle_normals, [[0, 0, 0]]); + assert_eq!(placed.normals, [[1.0, 0.0, 0.0]]); + } + + impl Matrix { + fn transformed_normal(&self, mesh: &TriangleMesh) -> [f64; 3] { + mesh.transformed(self).normals.first().copied().unwrap_or_default() + } + } +} diff --git a/crates/prc/src/lib.rs b/crates/prc/src/lib.rs index fd49302234..8e1a749944 100644 --- a/crates/prc/src/lib.rs +++ b/crates/prc/src/lib.rs @@ -3,7 +3,9 @@ //! //! Layer L0, standalone: this crate depends on no other PdfCraft crate. It reads the //! uncompressed container, inflates each section, and decodes the bit-level primitives and -//! the schema a newer producer uses to describe fields this reader does not know. +//! the schema a newer producer uses to describe fields this reader does not know; it decodes +//! tessellations and walks the product tree into placed, coloured meshes +//! ([`PrcFile::placements`], [`PrcFile::model_tree`]). //! //! Clause numbers in `[WD x.y]` refer to the PRC working draft, ISO/TC 171/SC 2 N570. //! @@ -18,13 +20,17 @@ mod bits; mod container; mod entity; mod error; +mod geometry; mod schema; mod tess; #[cfg(test)] mod testw; +mod tree; pub use bits::BitReader; pub use container::{FileStructure, MAX_FILE_STRUCTURES, MAX_INFLATED_BYTES, PRC_READER_VERSION, PrcFile, PrcHeader, SectionKind, UniqueId}; pub use error::PrcError; +pub use geometry::Matrix; pub use schema::Schema; pub use tess::{Tessellation, TriangleMesh, TriangleStyle}; +pub use tree::{EntityOverrides, MAX_PLACEMENTS, ModelNode, NameSource, Placement, Rgba, StyleAlpha}; diff --git a/crates/prc/src/schema.rs b/crates/prc/src/schema.rs index 686cc8e056..ba58474f3d 100644 --- a/crates/prc/src/schema.rs +++ b/crates/prc/src/schema.rs @@ -213,6 +213,8 @@ impl Exec<'_, '_, '_> { self.repeat(prog, pos, count, run, depth)?; } 16 => { + // prc-rs: reads SimpleFor's count as an Integer; this reader deliberately reads an + // UnsignedInteger (module doc). let count = if run { i64::from(self.r.unsigned_integer()?) } else { 0 }; self.repeat(prog, pos, count, run, depth)?; } diff --git a/crates/prc/src/tess.rs b/crates/prc/src/tess.rs index 42704de652..f41ea0a260 100644 --- a/crates/prc/src/tess.rs +++ b/crates/prc/src/tess.rs @@ -416,4 +416,4 @@ fn point_at(pts: &[[f64; 3]], idx: u32) -> Result<[f64; 3], PrcError> { } #[cfg(test)] -mod tests; +pub(crate) mod tests; diff --git a/crates/prc/src/tess/tests.rs b/crates/prc/src/tess/tests.rs index ee202e6a2a..ad7f497e82 100644 --- a/crates/prc/src/tess/tests.rs +++ b/crates/prc/src/tess/tests.rs @@ -223,6 +223,13 @@ fn too_many_texture_sets_are_refused_before_anything_is_allocated() { assert!(decode(§ion(1, &ok), &Schema::default(), 8137).is_ok()); } +/// A tessellation section holding one TESS_3D: [`SQUARE`] as two faces of one triangle each. +pub(crate) fn two_face_square() -> W { + let mut b = W::default(); + tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9], faces: &[(0x2, 0, &[1]), (0x2, 6, &[1])], ..Mesh::default() }); + section(1, &b) +} + #[test] fn two_faces_partition_the_triangles() { let m = one(&[0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9], &[(0x2, 0, &[1]), (0x2, 6, &[1])], 0); diff --git a/crates/prc/src/tree/mod.rs b/crates/prc/src/tree/mod.rs new file mode 100644 index 0000000000..6967ba40ea --- /dev/null +++ b/crates/prc/src/tree/mod.rs @@ -0,0 +1,293 @@ +//! The product tree: the tree section (`FileStructureTree`, 304), the globals' styles and +//! coordinate systems (303) and the model file (301), walked into placed tessellations +//! [WD 7.3.3-7.3.11, 7.5, 7.6]. +//! +//! Markups, views, scene lights and clipping planes are not read past; a tree carrying any +//! of them is [`PrcError::Unsupported`]. + +// prc-rs: where the working draft leaves an entity's field order open, the order here is +// prc-rs's format description's; each such entity's reader carries its own marker. + +use std::sync::Arc; + +use crate::bits::BitReader; +use crate::container::offset; +use crate::entity::{Ctx, malformed}; +use crate::geometry::{Matrix, Vec3}; +use crate::{PrcError, PrcFile, Schema, SectionKind, UniqueId}; + +mod node; +mod overrides; +mod read; +mod style; +mod walk; + +pub use node::{ModelNode, NameSource}; +pub use overrides::EntityOverrides; +use overrides::{Override, Target}; +pub(crate) use style::Graphics; +use style::{Globals, Paint, resolve_style}; +pub use style::{Rgba, StyleAlpha}; +use walk::{At, Walk}; + +const BASE_WITH_GRAPHICS: u32 = 2; +const MODEL_FILE: u32 = 301; +const FILE_STRUCTURE: u32 = 302; +const FILE_STRUCTURE_GLOBALS: u32 = 303; +const FILE_STRUCTURE_TREE: u32 = 304; +const PRODUCT_OCCURRENCE: u32 = 310; +const PART_DEFINITION: u32 = 311; +const FILTER: u32 = 320; +const CARTESIAN_TRANSFORMATION: u32 = 202; +const ENTITY_REFERENCE: u32 = 203; +const REFERENCE_ON_PRC_BASE: u32 = 205; +const REFERENCE_ON_TOPOLOGY: u32 = 206; +const GENERAL_TRANSFORMATION: u32 = 207; +const RI_BREP_MODEL: u32 = 232; +const RI_CURVE: u32 = 233; +const RI_DIRECTION: u32 = 234; +const RI_PLANE: u32 = 235; +const RI_POINT_SET: u32 = 236; +const RI_POLY_BREP_MODEL: u32 = 237; +const RI_POLY_WIRE: u32 = 238; +const RI_SET: u32 = 239; +const RI_COORDINATE_SYSTEM: u32 = 240; +const STYLE: u32 = 701; +const MATERIAL: u32 = 702; +const PICTURE: u32 = 703; +const TEXTURE_APPLICATION: u32 = 711; +const TEXTURE_DEFINITION: u32 = 712; +const TEXTURE_TRANSFORMATION: u32 = 713; +const LINE_PATTERN: u32 = 721; +const SCENE_DISPLAY_PARAMETERS: u32 = 741; +const CAMERA: u32 = 742; +/// prc-rs: a scene's `is_absolute` exists from this authoring version. +const ABSOLUTE_SCENE_FROM: u32 = 8137; +/// `TextureDefinition` mapping type "operator" and function "blend", 0-based +/// (pdf-issues #485). +const TEXTURE_MAPPING_OPERATOR: i32 = 3; +const TEXTURE_FUNCTION_BLEND: i32 = 3; +/// Texture application mode: alpha test [WD 7.5.7]. +const TEXTURE_ALPHA_TEST: u8 = 0x02; + +/// Ceiling on `RI_Set` nesting, the occurrence walk's depth and a prototype chain's length. +const MAX_DEPTH: usize = 64; +/// Ceiling on occurrences visited in one walk: prototypes share subtrees, so a small file can +/// describe an exponential walk. +const MAX_VISITS: usize = 1 << 20; +/// Ceiling on placements one walk yields. +pub const MAX_PLACEMENTS: usize = 1 << 20; + +/// `GraphicsContent` behaviour bits [WD 7.2.4.2]. +const SHOW: u16 = 0x0001; +const SON_HERIT_COLOR: u16 = 0x0008; +const FATHER_HERIT_COLOR: u16 = 0x0010; +const REMOVED: u16 = 0x2000; +/// `product_behavior` SUPPRESSED [WD 7.3.10]. +const SUPPRESSED: u8 = 0x01; + +/// Per file structure, entry `b` is the paint of biased style `b`. +type Palettes = Arc<[Arc<[Option]>]>; + +/// A tessellation drawn at a place in the model: world = [`Self::matrix`] × the +/// tessellation's own coordinates. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct Placement { + /// Index into [`PrcFile::file_structures`]. + pub file_structure: usize, + /// Index into that file structure's [`tessellations`](crate::FileStructure::tessellations). + pub tessellation: usize, + /// The occurrence path's locations, then the representation item's coordinate systems. + pub matrix: Matrix, + /// The colour of the style the tree resolves for this item; `None` when no style + /// reaches it or the style names no colour this reader resolves (a textured material + /// without a base). Faces with their own style are coloured by + /// [`Self::triangle_colours`]. + pub colour: Option, + /// The graphics from the root to this item, outermost first. + chain: Vec, + palettes: Palettes, + /// An occurrence's entity reference replacing the item's style. + item_override: Option, + /// Per face index, sorted, an entity reference replacing that face's style. + face_overrides: Vec<(u32, Graphics)>, +} + +/// The paint of the style `won` names, in its file structure's globals. +fn paint_of(palettes: &Palettes, won: Graphics) -> Option { + palettes.get(won.fs)?.get(offset(won.style)).copied().flatten() +} + +impl Placement { + /// Per triangle of `mesh` (this placement's tessellation), its colour as + /// [`Self::colour`] describes, with each face's own style taking part in the inheritance + /// [WD 7.2.4, 7.8.6]; `None` when no face of `mesh` carries a style or an override, so + /// every triangle is [`Self::colour`]. + #[must_use] + pub fn triangle_colours(&self, mesh: &crate::TriangleMesh) -> Option>> { + if mesh.triangle_styles.is_empty() && self.face_overrides.is_empty() { + return None; + } + let faces = overrides::triangle_faces(mesh); + let by_face = |k: usize| { + let f = faces.get(k)?; + let i = self.face_overrides.binary_search_by_key(f, |(i, _)| *i).ok()?; + self.face_overrides.get(i).map(|(_, g)| *g) + }; + let colour = |won| paint_of(&self.palettes, won).map(|p| p.rgba); + if mesh.triangle_styles.is_empty() { + let whole = self.item_override.unwrap_or_else(|| resolve_style(&self.chain)); + return Some((0..mesh.triangles.len()).map(|k| colour(by_face(k).unwrap_or(whole))).collect()); + } + let mut chain = self.chain.clone(); + chain.push(Graphics::default()); + let looks = mesh.triangle_styles.iter().enumerate().map(|(k, s)| { + if let Some(won) = by_face(k).or(self.item_override) { + return colour(won); + } + if let Some(last) = chain.last_mut() { + *last = Graphics { style: s.style, bits: s.behaviour, fs: self.file_structure }; + } + colour(resolve_style(&chain)) + }); + Some(looks.collect()) + } +} + +/// A representation item that can be drawn, flattened out of any `RI_Set`. +#[derive(Debug, Clone, PartialEq)] +struct Item { + /// `index_local_coordinate_system + 1` of each enclosing set and the item, outermost + /// first; 0 entries dropped. + local: Vec, + /// `index_tessellation + 1`; never 0 here. + tessellation: u32, + /// The part's graphics, each enclosing set's, then the item's own. + graphics: Vec, + /// The PRC unique id of each enclosing set, then the item's own. + uids: Vec, + /// A B-rep model's stored `(context, body)` ids [WD 7.6.2]. + brep: Option<(u32, u32)>, +} + +/// A `ProductOccurrence` (310) as far as placing and naming. +#[derive(Debug, Clone, PartialEq, Default)] +struct Product { + part: u32, + prototype: u32, + prototype_fs: Option, + external: u32, + external_fs: Option, + sons: Vec, + /// Its own graphics hide it (Show clear or Removed set). + hidden: bool, + suppressed: bool, + graphics: Graphics, + location: Option, + name: Option, + /// Its entity references that name a target. + overrides: Vec, +} + +/// One file structure's tree section. +#[derive(Debug, Clone, PartialEq, Default)] +struct Tree { + parts: Vec>, + /// Each part definition's name, parallel to `parts`. + part_names: Vec>, + products: Vec, +} + +/// The model file's roots: file structure id and biased occurrence index. +#[derive(Debug, Clone, PartialEq)] +struct ModelFile { + roots: Vec<(UniqueId, u32)>, +} + +impl PrcFile { + /// Where each drawable tessellation sits in the model: the product-occurrence tree walked + /// from the model file's roots, each occurrence's location composed onto its father's, + /// and each representation item's local coordinate systems applied last [WD 7.3.10.1, + /// 7.6.3.2], coloured with [`StyleAlpha::ZeroUnset`] and entity references applied by + /// [`EntityOverrides::Subtree`]. Suppressed occurrences and hidden entities are left + /// out; a tessellation several occurrences draw appears once per occurrence. Empty when + /// the file stores no tree ([`Self::stores_tree`]). + /// + /// # Errors + /// [`PrcError::Unsupported`] for a tree carrying markups (PMI), views, scene lights or + /// clipping planes, or, when an item is placed by a reference coordinate system, globals + /// carrying fonts or fill patterns; [`PrcError::Malformed`] for an index naming no entity, + /// a cycle of occurrences or prototypes, or a tree past [`MAX_PLACEMENTS`] or the depth + /// ceiling; read errors as [`crate::FileStructure::tessellations`]. + pub fn placements(&self) -> Result, PrcError> { + self.placements_with(StyleAlpha::default(), EntityOverrides::default()) + } + + /// [`Self::placements`], combining style transparency with material alpha by `rule` and + /// applying occurrences' entity references by `scope`. + /// + /// # Errors + /// As [`Self::placements`]. + pub fn placements_with(&self, rule: StyleAlpha, scope: EntityOverrides) -> Result, PrcError> { + Ok(self.walk(rule, scope)?.0) + } + + /// The assembly tree as a model-tree panel lists it: every product occurrence the model + /// file's roots reach, depth first, with its display name and stored visibility. Hidden + /// and suppressed subtrees are listed (with `drawn` false) though [`Self::placements`] + /// leaves them out; each node's `placements` range indexes that list, so attributes each + /// placed mesh to its part. Empty when the file stores no tree. + /// + /// # Errors + /// As [`Self::placements`]. + pub fn model_tree(&self) -> Result, PrcError> { + Ok(self.walk(StyleAlpha::default(), EntityOverrides::default())?.1) + } + + /// Whether the file stores a tree: its model file holds an entity. ISO 14739-1 requires + /// one, but a tessellation-only file still draws (unplaced), so its absence reads as an + /// empty tree, not an error. + #[must_use] + pub fn stores_tree(&self) -> bool { + self.model_file.iter().any(|&b| b != 0) + } + + fn walk(&self, rule: StyleAlpha, scope: EntityOverrides) -> Result<(Vec, Vec), PrcError> { + if !self.stores_tree() { + return Ok((Vec::new(), Vec::new())); + } + let mut trees = Vec::with_capacity(self.file_structures.len()); + for fs in &self.file_structures { + let schema = fs.schema()?; + let tree = Ctx::new(BitReader::new(fs.section(SectionKind::Tree)), &schema, fs.authoring_version).file_structure_tree()?; + // The globals carry the coordinate systems and the colours; colours alone are not + // worth refusing the placements over. + let globals = (|| { + let mut r = BitReader::new(fs.section(SectionKind::Globals)); + Schema::read(&mut r)?; + Ctx::new(r, &schema, fs.authoring_version).globals() + })(); + let globals = match globals { + Ok(g) => g, + Err(e) if tree.parts.iter().flatten().any(|i| !i.local.is_empty()) => return Err(e), + Err(_) => Globals::default(), + }; + trees.push((fs.id, tree, globals)); + } + let mut r = BitReader::new(&self.model_file); + let schema = Schema::read(&mut r)?; + let model = Ctx::new(r, &schema, self.header.authoring_version).model_file()?; + let mut walk = Walk::new(trees.iter().map(|(id, t, g)| (*id, t, g)).collect(), rule, scope); + for (id, root) in model.roots { + let fs = trees.iter().position(|t| t.0 == id).ok_or_else(|| malformed("a root names an unknown file structure".into()))?; + if let Some(index) = root.checked_sub(1) { + walk.occurrence(fs, offset(index), &Matrix::IDENTITY, &[], At::ROOT)?; + } + } + Ok((walk.out, walk.nodes)) + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/prc/src/tree/node.rs b/crates/prc/src/tree/node.rs new file mode 100644 index 0000000000..19a8e04e5c --- /dev/null +++ b/crates/prc/src/tree/node.rs @@ -0,0 +1,68 @@ +//! The assembly tree as a list: what a model-tree panel shows. + +use std::ops::Range; + +/// Where a [`ModelNode`]'s name came from. PRC does not say which name a +/// viewer should show; this reader takes the first present of these, in order. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum NameSource { + /// The occurrence's own name. + Occurrence, + /// The name of an occurrence in its prototype chain. + Prototype, + /// The name of the part definition it draws. + Part, + /// No name anywhere: [`ModelNode::name`] is `None`. + Unnamed, +} + +/// One product occurrence of a PRC model's assembly tree, as +/// [`PrcFile::model_tree`](crate::PrcFile::model_tree) lists it: depth +/// first, a parent before its children, children in file order. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct ModelNode { + /// The name to show, by [`Self::name_from`]; `None` when unnamed. + pub name: Option, + /// Where [`Self::name`] came from. + pub name_from: NameSource, + /// Index of the parent node in the list; `None` for a root. + pub parent: Option, + /// 0 for a root. + pub depth: usize, + /// Index into [`crate::PrcFile::file_structures`] of the structure + /// holding this occurrence. + pub file_structure: usize, + /// The occurrence's 0-based index in that structure's tree. + pub occurrence: usize, + /// The occurrence's own graphics hide it (Show clear, or Removed) + /// [WD 7.2.4.2]: the visibility the file stores, which a viewer opens on. + pub hidden: bool, + /// The occurrence is suppressed (`product_behavior`) [WD 7.3.10]. + pub suppressed: bool, + /// Whether anything of this node is drawn: false when it or an + /// ancestor is hidden or suppressed. + pub drawn: bool, + /// Whether the occurrence (or its prototype) carries a part definition, + /// i.e. geometry of its own rather than only children. + pub has_part: bool, + /// The placements this node and its descendants draw: indices into + /// [`PrcFile::placements`](crate::PrcFile::placements) (or + /// [`placements_with`](crate::PrcFile::placements_with), which yields the same order): + /// the meshes attributed to this occurrence's part and its descendants' parts. Empty when + /// nothing under it is drawn. + pub placements: Range, +} + +impl ModelNode { + /// The label the node is listed under: its name, or `occurrence F:I` + /// (file structure, occurrence) when it has none. + #[must_use] + pub fn label(&self) -> String { + match &self.name { + Some(name) => name.clone(), + None => format!("occurrence {}:{}", self.file_structure, self.occurrence), + } + } +} diff --git a/crates/prc/src/tree/overrides.rs b/crates/prc/src/tree/overrides.rs new file mode 100644 index 0000000000..a85a83ba09 --- /dev/null +++ b/crates/prc/src/tree/overrides.rs @@ -0,0 +1,128 @@ +//! Product-occurrence entity references: the colour, visibility and placement an occurrence +//! imposes on an entity it reaches [WD 7.3.10.1, 7.4.4]. + +use super::{Graphics, Item, RI_BREP_MODEL, RI_COORDINATE_SYSTEM, UniqueId}; + +/// `PRC_TYPE_TOPO_Face` [WD 6.2]. +const TOPO_FACE: u32 = 149; + +/// Which entities an occurrence's entity references reach. ISO 14739-1 says a reference +/// overrides the referenced entity's properties, and not where. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum EntityOverrides { + /// Only where the occurrence's own subtree places the target, so one instance of a + /// shared part can be coloured alone. + #[default] + Subtree, + /// Every placement of the target anywhere in the model. + Everywhere, + /// Read past them: every entity keeps its own and inherited graphics. + Ignore, +} + +/// One `MISC_EntityReference` held by a product occurrence. +#[derive(Debug, Clone, PartialEq)] +pub(super) struct Override { + /// The reference's graphics; style 0 leaves the target's colour alone. + pub(super) graphics: Graphics, + /// The graphics hide the target (Show clear or Removed set). + pub(super) hidden: bool, + /// `index_local_coordinate_system + 1` replacing the target's; 0 none. + pub(super) local: u32, + pub(super) target: Target, +} + +/// What an entity reference names. +#[derive(Debug, Clone, PartialEq)] +pub(super) enum Target { + /// `ReferenceOnPRCBase`: the entity of type `kind` whose PRC unique id is `uid` in + /// structure `fs` (`None`: the occurrence's own). + Entity { kind: u32, fs: Option, uid: u32 }, + /// `ReferenceOnTopology` into the B-rep stored as `(context, body)`: its topological + /// items of type `kind` at `indices` (0-based). With `fs` `None` it matches that B-rep in + /// any structure the subtree reaches: producers write "same structure" for an assembly's + /// reference into a part's own structure. + Topology { kind: u32, fs: Option, brep: (u32, u32), indices: Vec }, +} + +impl Target { + /// The structure the reference names, if it names one. + pub(super) fn fs(&self) -> Option { + match self { + Target::Entity { fs, .. } | Target::Topology { fs, .. } => *fs, + } + } +} + +/// An entity reference in force: the structure of the occurrence holding it, the structure +/// it names (`None`: any), and the reference. +pub(super) type Active<'t> = (usize, Option, &'t Override); + +/// The overrides that reach one representation item. +#[derive(Debug, Default)] +pub(super) struct Reached { + /// Graphics replacing the item's resolved style, in its owner's structure. + pub(super) item: Option, + pub(super) hidden: bool, + /// The owner's structure and its local coordinate system. + pub(super) local: Option<(usize, u32)>, + /// Per face index, sorted, graphics replacing that face's style. + pub(super) faces: Vec<(u32, Graphics)>, +} + +impl Reached { + /// Applies `active` innermost first, so an outer occurrence's override wins over an + /// inner one's, as an assembly's appearance beats its sub-assembly's. + pub(super) fn collect(active: &[Active<'_>], fs: usize, item: &Item) -> Self { + let mut r = Reached::default(); + // A map, not a list searched per index: one reference may name millions of faces. + let mut faces = std::collections::BTreeMap::new(); + for &(owner, named, ov) in active.iter().rev() { + let in_fs = named.is_none_or(|n| n == fs); + let g = Graphics { fs: owner, ..ov.graphics }; + match &ov.target { + Target::Entity { kind, uid, .. } => { + if !in_fs || !is_item(*kind) || !item.uids.contains(uid) { + continue; + } + if ov.graphics.style != 0 { + r.item = Some(g); + } + r.hidden = ov.hidden; + if ov.local != 0 { + r.local = Some((owner, ov.local)); + } + } + Target::Topology { kind, brep, indices, .. } => { + if !in_fs || *kind != TOPO_FACE || item.brep != Some(*brep) || ov.graphics.style == 0 { + continue; + } + for &f in indices { + faces.insert(f, g); + } + } + } + } + r.faces = faces.into_iter().collect(); + r + } +} + +/// Representation-item types a `ReferenceOnPRCBase` can colour. +fn is_item(kind: u32) -> bool { + (RI_BREP_MODEL..=RI_COORDINATE_SYSTEM).contains(&kind) +} + +/// Per triangle of `mesh`, the index of the `TESS_Face` it was read from (`u32::MAX` for +/// none). +pub(super) fn triangle_faces(mesh: &crate::TriangleMesh) -> Vec { + let mut out = vec![u32::MAX; mesh.triangles.len()]; + for (i, range) in mesh.faces.iter().enumerate() { + let face = u32::try_from(i).unwrap_or(u32::MAX); + for f in out.get_mut(range.clone()).into_iter().flatten() { + *f = face; + } + } + out +} diff --git a/crates/prc/src/tree/read.rs b/crates/prc/src/tree/read.rs new file mode 100644 index 0000000000..6c8047b989 --- /dev/null +++ b/crates/prc/src/tree/read.rs @@ -0,0 +1,451 @@ +//! The tree section's and the model file's entities: part definitions, representation items, +//! product occurrences and their transformations and entity references [WD 7.3, 7.4, 7.6]. + +use super::{ + ABSOLUTE_SCENE_FROM, BASE_WITH_GRAPHICS, CAMERA, CARTESIAN_TRANSFORMATION, ENTITY_REFERENCE, FILE_STRUCTURE, FILE_STRUCTURE_TREE, FILTER, + GENERAL_TRANSFORMATION, Graphics, Item, MAX_DEPTH, MODEL_FILE, Matrix, ModelFile, Override, PART_DEFINITION, PRODUCT_OCCURRENCE, Product, + REFERENCE_ON_PRC_BASE, REFERENCE_ON_TOPOLOGY, REMOVED, RI_BREP_MODEL, RI_COORDINATE_SYSTEM, RI_CURVE, RI_DIRECTION, RI_PLANE, RI_POINT_SET, + RI_POLY_BREP_MODEL, RI_POLY_WIRE, RI_SET, SCENE_DISPLAY_PARAMETERS, SHOW, SUPPRESSED, Target, Tree, UniqueId, Vec3, +}; +use crate::PrcError; +use crate::entity::{Ctx, malformed}; + +impl Ctx<'_, '_> { + pub(super) fn vector3(&mut self) -> Result { + Ok(Vec3([self.r.double()?, self.r.double()?, self.r.double()?])) + } + + pub(super) fn unique_id(&mut self) -> Result { + let mut id = [0; 4]; + for v in &mut id { + *v = self.r.unsigned_integer()?; + } + Ok(UniqueId(id)) + } + + /// `ContentPRCRefBase` [WD 7.2.3.2; pdf-issues #697]: the base, then the CAD ids and the + /// PRC unique id, which becomes [`Ctx::uid`]. + pub(super) fn content_prc_ref_base(&mut self) -> Result<(), PrcError> { + self.content_prc_base()?; + self.r.unsigned_integer()?; // CAD identifier + self.r.unsigned_integer()?; // CAD persistent identifier + self.uid = self.r.unsigned_integer()?; + Ok(()) + } + + /// `PRCBaseWithGraphics` [WD 7.2.4; pdf-issues #405]: whether the entity's own graphics + /// hide it (Show clear or Removed set), and its graphics. Reused graphics + /// (`same_graphics`) are the current graphics and never hide. + pub(super) fn base_with_graphics(&mut self) -> Result<(bool, Graphics), PrcError> { + self.content_prc_ref_base()?; + let hidden = if self.r.bit()? { + false + } else { + self.r.unsigned_integer()?; // layer + 1 + let style = self.r.unsigned_integer()?; + let lo = u16::from(self.r.character()?); + let hi = u16::from(self.r.character()?); + let bits = lo | hi << 8; + self.graphics = Graphics { style, bits, fs: 0 }; + bits & SHOW == 0 || bits & REMOVED != 0 + }; + // prc-rs: schema additions to the base itself follow the graphics. + self.schema.skip_added_fields(BASE_WITH_GRAPHICS, &mut self.r)?; + Ok((hidden, self.graphics)) + } + + /// `CartesianTransformation` (202) or `GeneralTransformation` (207), type code included + /// [WD 7.4.8, 7.4.9, 7.4.11; pdf-issues #430]. + pub(super) fn transformation(&mut self) -> Result { + let t = self.r.unsigned_integer()?; + let m = match t { + GENERAL_TRANSFORMATION => { + // Column-major [WD 7.4.9]. + let mut m = Matrix::IDENTITY; + for col in 0..4 { + for row in &mut m.0 { + if let Some(c) = row.get_mut(col) { + *c = self.r.double()?; + } + } + } + m + } + CARTESIAN_TRANSFORMATION => self.transformation_3d()?, + t => return Err(malformed(format!("entity type {t} as a transformation"))), + }; + self.schema.skip_added_fields(t, &mut self.r)?; + Ok(m) + } + + /// The embedded `Transformation3D` [WD 7.4.11.1]: a behaviour byte, then each field its + /// bits name. + fn transformation_3d(&mut self) -> Result { + let b = self.r.character()?; + let mut m = Matrix::IDENTITY; + let set_col = |m: &mut Matrix, col: usize, v: Vec3| { + for (row, x) in m.0.iter_mut().zip(v.0) { + if let Some(c) = row.get_mut(col) { + *c = x; + } + } + }; + if b & 0x01 != 0 { + let t = self.vector3()?; + set_col(&mut m, 3, t); + } + if b & 0x20 != 0 { + for col in 0..3 { + let v = self.vector3()?; + set_col(&mut m, col, v); + } + } else if b & 0x02 != 0 { + let x = self.vector3()?; + let y = self.vector3()?; + // Mirrored (0x04): the third axis is y × x. + let z = if b & 0x04 != 0 { y.cross(x) } else { x.cross(y) }; + set_col(&mut m, 0, x); + set_col(&mut m, 1, y); + set_col(&mut m, 2, z); + } + let mut scale = [1.0; 3]; + if b & 0x10 != 0 { + scale = self.vector3()?.0; + } + // prc-rs: with both scale bits set, both are read and multiply. + if b & 0x08 != 0 { + let s = self.r.double()?; + scale = scale.map(|v| v * s); + } + for row in m.0.iter_mut().take(3) { + for (c, s) in row.iter_mut().zip(scale) { + *c *= s; + } + } + if b & 0x40 != 0 { + let h = [self.r.double()?, self.r.double()?, self.r.double()?, self.r.double()?]; + if let Some(r) = m.0.get_mut(3) { + *r = h; + } + } + Ok(m) + } + + /// A polymorphic representation item [WD 7.6]; drawable ones are pushed onto `out` with + /// `local` (the enclosing sets' coordinate systems) prefixed. + // prc-rs: the item's own fields follow its local coordinate system and tessellation. + fn representation_item(&mut self, at: &Item, out: &mut Vec, depth: usize) -> Result<(), PrcError> { + if depth > MAX_DEPTH { + return Err(malformed("representation sets nest too deep".into())); + } + let t = self.r.unsigned_integer()?; + let (hidden, own) = self.base_with_graphics()?; + let mut item = at.clone(); + item.uids.push(self.uid); + item.graphics.push(own); + let cs = self.r.unsigned_integer()?; + let tess = self.r.unsigned_integer()?; + if cs != 0 { + item.local.push(cs); + } + let (drawable, brep) = if t == RI_SET { + let n = self.count(1, "representation set")?; + let mut members = Vec::new(); + for _ in 0..n { + self.representation_item(&item, &mut members, depth + 1)?; + } + if !hidden { + out.extend(members); + } + (false, None) + } else { + self.leaf_item_fields(t)? + }; + self.schema.skip_added_fields(t, &mut self.r)?; + self.user_data()?; + if drawable && !hidden && tess != 0 { + out.push(Item { tessellation: tess, brep, ..item }); + } + Ok(()) + } + + /// The type-specific fields of a representation item other than `RI_Set` [WD 7.6]; + /// whether the item can be drawn, and a B-rep model's `(context, body)`. + fn leaf_item_fields(&mut self, t: u32) -> Result<(bool, Option<(u32, u32)>), PrcError> { + match t { + RI_BREP_MODEL => { + let brep = self.exact_tolerance()?; + self.r.bit()?; // is_closed + return Ok((true, brep)); + } + RI_CURVE | RI_PLANE => { + self.exact_tolerance()?; + } + RI_DIRECTION => { + if self.r.bit()? { + self.vector3()?; // origin + } + self.vector3()?; + return Ok((false, None)); + } + RI_POINT_SET => { + let n = self.count(6, "point set")?; + for _ in 0..n { + self.vector3()?; + } + } + RI_POLY_BREP_MODEL => { + self.r.bit()?; // is_closed + } + RI_POLY_WIRE => {} + RI_COORDINATE_SYSTEM => { + self.transformation()?; + return Ok((false, None)); + } + t => return Err(malformed(format!("entity type {t} as a representation item"))), + } + Ok((true, None)) + } + + /// A B-rep, curve or plane item's optional exact-geometry `(context, body)` ids + /// [WD 7.6.2]. + fn exact_tolerance(&mut self) -> Result, PrcError> { + if self.r.bit()? { + return Ok(Some((self.r.unsigned_integer()?, self.r.unsigned_integer()?))); + } + Ok(None) + } + + /// `SceneDisplayParameters` (741) [WD 7.5.19]: read past. Lights and clipping planes are + /// [`PrcError::Unsupported`]. + // prc-rs: the scene's field order, and `is_absolute` from version 8137. + fn scene_display_parameters(&mut self) -> Result<(), PrcError> { + self.expect_type(SCENE_DISPLAY_PARAMETERS)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_active + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("scene lights")); + } + if self.r.bit()? { + self.camera()?; + } + if self.r.bit()? { + self.vector3()?; // rotation centre + } + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("scene clipping planes")); + } + self.r.unsigned_integer()?; // background style + 1 + self.r.unsigned_integer()?; // default style + 1 + let n = self.count(2, "scene default styles")?; + for _ in 0..n { + self.r.unsigned_integer()?; // entity type + self.r.unsigned_integer()?; // style + 1 + } + if self.version >= ABSOLUTE_SCENE_FROM { + self.r.bit()?; // is_absolute + } + self.schema.skip_added_fields(SCENE_DISPLAY_PARAMETERS, &mut self.r) + } + + /// `Camera` (742) [WD 7.5.20]: read past. + // prc-rs: the camera's field order. + fn camera(&mut self) -> Result<(), PrcError> { + self.expect_type(CAMERA)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_orthographic + for _ in 0..3 { + self.vector3()?; // position, look, up + } + for _ in 0..6 { + self.r.double()?; // x, y, ratio, near, far, zoom + } + self.schema.skip_added_fields(CAMERA, &mut self.r) + } + + /// `MarkupData` [WD 7.3.10.4]: only the empty form reads past. + fn markup_data(&mut self) -> Result<(), PrcError> { + for _ in 0..4 { + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("tree markups (PMI)")); + } + } + Ok(()) + } + + fn views(&mut self) -> Result<(), PrcError> { + if self.r.unsigned_integer()? != 0 { + return Err(PrcError::Unsupported("tree views")); + } + Ok(()) + } + + /// `MISC_EntityReference` (203) [WD 7.4.4, 7.4.10]; `None` when it names no target. + // prc-rs: the local coordinate system precedes the optional reference data. + fn entity_reference(&mut self) -> Result, PrcError> { + self.expect_type(ENTITY_REFERENCE)?; + let (hidden, graphics) = self.base_with_graphics()?; + let local = self.r.unsigned_integer()?; + let mut target = None; + if self.r.bit()? { + match self.r.unsigned_integer()? { + REFERENCE_ON_PRC_BASE => { + let kind = self.r.unsigned_integer()?; + let fs = self.file_identifier()?; + let uid = self.r.unsigned_integer()?; + target = Some(Target::Entity { kind, fs, uid }); + } + REFERENCE_ON_TOPOLOGY => target = self.reference_on_topology()?, + t => return Err(malformed(format!("entity type {t} as reference data"))), + } + } + self.schema.skip_added_fields(ENTITY_REFERENCE, &mut self.r)?; + self.user_data()?; + Ok(target.map(|target| Override { graphics, hidden, local, target })) + } + + /// `ReferenceOnTopology` (206) after its type code; `None` without a body. + fn reference_on_topology(&mut self) -> Result, PrcError> { + let kind = self.r.unsigned_integer()?; + if !self.r.bit()? { + return Ok(None); + } + let fs = self.file_identifier()?; + let brep = (self.r.unsigned_integer()?, self.r.unsigned_integer()?); + let indices = self.uints("topology reference indices")?; + Ok(Some(Target::Topology { kind, fs, brep, indices })) + } + + /// `ASM_Filter` (320) [WD 7.3.12]: read past. + // prc-rs: the filter's field order. + fn filter(&mut self) -> Result<(), PrcError> { + self.expect_type(FILTER)?; + self.content_prc_ref_base()?; + self.r.bit()?; // is_active + self.r.bit()?; // layers inclusive + self.uints("filter layers")?; + self.r.bit()?; // entities inclusive + let n = self.count(1, "filter entities")?; + for _ in 0..n { + self.entity_reference()?; + } + self.schema.skip_added_fields(FILTER, &mut self.r)?; + self.user_data() + } + + /// `PartDefinition` (311): its drawable items and its name. + fn part_definition(&mut self) -> Result<(Vec, Option), PrcError> { + self.expect_type(PART_DEFINITION)?; + let (hidden, own) = self.base_with_graphics()?; + let name = self.name.clone(); + self.vector3()?; // bounding box + self.vector3()?; + let n = self.count(1, "representation items")?; + let mut items = Vec::new(); + let at = Item { local: Vec::new(), tessellation: 0, graphics: vec![own], uids: Vec::new(), brep: None }; + for _ in 0..n { + self.representation_item(&at, &mut items, 0)?; + } + self.markup_data()?; + self.views()?; + self.schema.skip_added_fields(PART_DEFINITION, &mut self.r)?; + self.user_data()?; + if hidden { + items.clear(); + } + Ok((items, name)) + } + + /// `FileIdentifier` [WD 7.3.10.2.2]: `None` for the same structure. + fn file_identifier(&mut self) -> Result, PrcError> { + if self.r.bit()? { Ok(None) } else { self.unique_id().map(Some) } + } + + /// `ProductOccurrence` (310) [WD 7.3.10]. + fn product_occurrence(&mut self) -> Result { + self.expect_type(PRODUCT_OCCURRENCE)?; + let (hidden, graphics) = self.base_with_graphics()?; + let mut p = Product { hidden, graphics, name: self.name.clone(), part: self.r.unsigned_integer()?, ..Product::default() }; + p.prototype = self.r.unsigned_integer()?; + if p.prototype != 0 { + p.prototype_fs = self.file_identifier()?; + } + p.external = self.r.unsigned_integer()?; + if p.external != 0 { + p.external_fs = self.file_identifier()?; + } + p.sons = self.uints("son occurrences")?; + p.suppressed = self.r.character()? & SUPPRESSED != 0; + // ProductInformation [WD 7.3.10.3]: unit flag, unit, load status, information. + self.r.bit()?; + self.r.double()?; + self.r.character()?; + self.r.integer()?; + if self.r.bit()? { + p.location = Some(self.transformation()?); + } + let n = self.count(1, "entity references")?; + for _ in 0..n { + p.overrides.extend(self.entity_reference()?); + } + self.markup_data()?; + self.views()?; + if self.r.bit()? { + self.filter()?; + } + let n = self.count(1, "display filters")?; + for _ in 0..n { + self.filter()?; + } + let n = self.count(1, "scene display parameters")?; + for _ in 0..n { + self.scene_display_parameters()?; + } + self.schema.skip_added_fields(PRODUCT_OCCURRENCE, &mut self.r)?; + self.user_data()?; + Ok(p) + } + + /// `FileStructureTree` (304) [WD 7.3.6], with its `FileStructure` (302) inside. + pub(super) fn file_structure_tree(&mut self) -> Result { + self.expect_type(FILE_STRUCTURE_TREE)?; + self.content_prc_base()?; + let n = self.count(1, "part definitions")?; + let mut tree = Tree::default(); + for _ in 0..n { + let (items, name) = self.part_definition()?; + tree.parts.push(items); + tree.part_names.push(name); + } + let n = self.count(1, "product occurrences")?; + for _ in 0..n { + tree.products.push(self.product_occurrence()?); + } + self.expect_type(FILE_STRUCTURE)?; + self.content_prc_base()?; + self.r.unsigned_integer()?; // next_available_index + self.r.unsigned_integer()?; // product_occurrence_index + 1 (the structure's root) + self.schema.skip_added_fields(FILE_STRUCTURE, &mut self.r)?; + self.schema.skip_added_fields(FILE_STRUCTURE_TREE, &mut self.r)?; + self.user_data()?; + Ok(tree) + } + + /// `ModelFile` (301) after its schema [WD 7.3.3; pdf-issues #724, #740]: `units_in_mm` is + /// always present. + pub(super) fn model_file(&mut self) -> Result { + self.expect_type(MODEL_FILE)?; + self.content_prc_base()?; + self.r.bit()?; // units_from_cad_file + self.r.double()?; // units_in_mm + let n = self.count(6, "root occurrences")?; + let mut roots = Vec::with_capacity(n); + for _ in 0..n { + let id = self.unique_id()?; + let index = self.r.unsigned_integer()?; + self.r.bit()?; // is_active + roots.push((id, index)); + } + Ok(ModelFile { roots }) + } +} diff --git a/crates/prc/src/tree/style.rs b/crates/prc/src/tree/style.rs new file mode 100644 index 0000000000..d313fb2f64 --- /dev/null +++ b/crates/prc/src/tree/style.rs @@ -0,0 +1,348 @@ +//! Graphics, styles, materials and the globals that hold them [WD 7.2.4, 7.3.5, 7.5]. + +use super::{ + FATHER_HERIT_COLOR, FILE_STRUCTURE_GLOBALS, LINE_PATTERN, MATERIAL, Matrix, PICTURE, RI_COORDINATE_SYSTEM, SON_HERIT_COLOR, STYLE, + TEXTURE_ALPHA_TEST, TEXTURE_APPLICATION, TEXTURE_DEFINITION, TEXTURE_FUNCTION_BLEND, TEXTURE_MAPPING_OPERATOR, TEXTURE_TRANSFORMATION, Vec3, +}; +use crate::PrcError; +use crate::container::offset; +use crate::entity::{Ctx, malformed}; + +/// One entity's `GraphicsContent` [WD 7.2.4]: `style` is `line_style_index + 1` (0 none), +/// `bits` the behaviour bits. `fs` is the file structure whose globals `style` indexes: the +/// one whose tree holds the entity. The walk sets it; a reader leaves it 0. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) struct Graphics { + pub(crate) style: u32, + pub(crate) bits: u16, + pub(crate) fs: usize, +} + +/// A straight (not premultiplied) colour, each channel 0-1; `a` is the opacity. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[non_exhaustive] +pub struct Rgba { + /// Red. + pub r: f64, + /// Green. + pub g: f64, + /// Blue. + pub b: f64, + /// Opacity: 0 transparent, 1 opaque. + pub a: f64, +} + +/// How a style's `transparency` combines with its material's diffuse alpha. ISO 14739-1 +/// defines both and not their combination, and gives a diffuse alpha of 0.0 no "unset" +/// meaning. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum StyleAlpha { + /// As [`Self::StyleWins`], except that a material diffuse alpha of exactly 0.0 under a + /// style with no transparency reads as unset, so opaque. Producers write 0.0 on the + /// materials of models meant to be seen; read literally, every part is invisible. + #[default] + ZeroUnset, + /// The style's transparency, when it has one, is the opacity; the material alpha, read + /// literally (0.0 invisible), applies only to a style without one. + StyleWins, + /// The opacity is material alpha × style transparency. + Multiply, +} + +/// A style's resolved colour, and whether its opacity comes from reading a zero material +/// alpha as unset. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct Paint { + pub(crate) rgba: Rgba, + pub(crate) alpha_unset: bool, +} + +/// A `Style` (701) as far as colour: `index` is `colour_or_material + 1`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Style { + pub(crate) is_material: bool, + pub(crate) index: u32, + pub(crate) transparency: Option, +} + +/// A `materials` entry as far as colour. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum Material { + /// `Material` (702): `diffuse + 1` (a colour index) and the diffuse alpha. + Plain { diffuse: u32, alpha: f64 }, + /// `TextureApplication` (711) [WD 7.5.6]: `material_generic_index + 1`, the material + /// under the texture. + Textured { base: u32 }, +} + +/// What `FileStructureGlobals` contributes to placing and colouring. +#[derive(Debug, Clone, PartialEq, Default)] +pub(crate) struct Globals { + /// Entry `i` places an item with biased local coordinate system `i + 1`. + pub(crate) systems: Vec, + /// RGB, `a` 1. + pub(crate) colours: Vec, + pub(crate) materials: Vec, + pub(crate) styles: Vec