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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76
2026-11-02 12:26:44 -05:00
co-authored by Claude Opus 5.5
parent 7f30266790
commit c571b13469
16 changed files with 2344 additions and 5 deletions
+12 -1
View File
@@ -14,6 +14,10 @@ PrcFile::parse_with_limit(data, max_inflated) -> Result<PrcFile, PrcError>
FileStructure::section(SectionKind) -> &[u8] // inflated globals / tree / tessellation / geometry / extra geometry
FileStructure::schema() -> Result<Schema, PrcError> // the globals section's leading schema
FileStructure::tessellations() -> Result<Vec<Tessellation>, PrcError> // meshes, wires, markups, compressed meshes
PrcFile::placements() / placements_with(StyleAlpha, EntityOverrides) -> Result<Vec<Placement>, PrcError>
PrcFile::model_tree() -> Result<Vec<ModelNode>, PrcError> // the assembly tree as a model-tree panel lists it
Placement::triangle_colours(&TriangleMesh) -> Option<Vec<Option<Rgba>>>
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.
+2
View File
@@ -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<f64, PrcError> {
// 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 {
+2 -1
View File
@@ -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::<Result<Vec<_>, _>>()?;
+6 -1
View File
@@ -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<String>,
/// 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.
+179
View File
@@ -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::<f64>().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()
}
}
}
+7 -1
View File
@@ -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};
+2
View File
@@ -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)?;
}
+1 -1
View File
@@ -416,4 +416,4 @@ fn point_at(pts: &[[f64; 3]], idx: u32) -> Result<[f64; 3], PrcError> {
}
#[cfg(test)]
mod tests;
pub(crate) mod tests;
+7
View File
@@ -223,6 +223,13 @@ fn too_many_texture_sets_are_refused_before_anything_is_allocated() {
assert!(decode(&section(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);
+293
View File
@@ -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<Paint>]>]>;
/// 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<Rgba>,
/// The graphics from the root to this item, outermost first.
chain: Vec<Graphics>,
palettes: Palettes,
/// An occurrence's entity reference replacing the item's style.
item_override: Option<Graphics>,
/// 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<Paint> {
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<Vec<Option<Rgba>>> {
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<u32>,
/// `index_tessellation + 1`; never 0 here.
tessellation: u32,
/// The part's graphics, each enclosing set's, then the item's own.
graphics: Vec<Graphics>,
/// The PRC unique id of each enclosing set, then the item's own.
uids: Vec<u32>,
/// 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<UniqueId>,
external: u32,
external_fs: Option<UniqueId>,
sons: Vec<u32>,
/// Its own graphics hide it (Show clear or Removed set).
hidden: bool,
suppressed: bool,
graphics: Graphics,
location: Option<Matrix>,
name: Option<String>,
/// Its entity references that name a target.
overrides: Vec<Override>,
}
/// One file structure's tree section.
#[derive(Debug, Clone, PartialEq, Default)]
struct Tree {
parts: Vec<Vec<Item>>,
/// Each part definition's name, parallel to `parts`.
part_names: Vec<Option<String>>,
products: Vec<Product>,
}
/// 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<Vec<Placement>, 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<Vec<Placement>, 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<Vec<ModelNode>, 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<Placement>, Vec<ModelNode>), 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;
+68
View File
@@ -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<String>,
/// Where [`Self::name`] came from.
pub name_from: NameSource,
/// Index of the parent node in the list; `None` for a root.
pub parent: Option<usize>,
/// 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<usize>,
}
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),
}
}
}
+128
View File
@@ -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<UniqueId>, 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<UniqueId>, brep: (u32, u32), indices: Vec<u32> },
}
impl Target {
/// The structure the reference names, if it names one.
pub(super) fn fs(&self) -> Option<UniqueId> {
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<usize>, &'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<Graphics>,
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<u32> {
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
}
+451
View File
@@ -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<Vec3, PrcError> {
Ok(Vec3([self.r.double()?, self.r.double()?, self.r.double()?]))
}
pub(super) fn unique_id(&mut self) -> Result<UniqueId, PrcError> {
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<Matrix, PrcError> {
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<Matrix, PrcError> {
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<Item>, 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<Option<(u32, u32)>, 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<Option<Override>, 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<Option<Target>, 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<Item>, Option<String>), 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<Option<UniqueId>, 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<Product, PrcError> {
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<Tree, PrcError> {
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<ModelFile, PrcError> {
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 })
}
}
+348
View File
@@ -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<u8>,
}
/// 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<Matrix>,
/// RGB, `a` 1.
pub(crate) colours: Vec<Rgba>,
pub(crate) materials: Vec<Material>,
pub(crate) styles: Vec<Style>,
}
/// The entry at biased index `biased` (`index + 1`); `None` for 0 or past the end.
pub(crate) fn biased<T>(list: &[T], biased: u32) -> Option<&T> {
list.get(offset(biased.checked_sub(1)?))
}
impl Globals {
/// The colour a style or material names by `biased` (`index + 1`). Stored indices count
/// the colour array's doubles, so `colours[i]` is stored as `3i + 1`; an index that is
/// not of that form names nothing.
fn colour(&self, biased: u32) -> Option<Rgba> {
let i = biased.checked_sub(1)?;
// pdf-issues #816: a colour index counts doubles, three per colour.
if i % 3 != 0 {
return None;
}
self.colours.get(offset(i / 3)).copied()
}
/// The colour of style `biased` (`line_style_index + 1`) under `rule`.
pub(crate) fn style_paint(&self, biased_style: u32, rule: StyleAlpha) -> Option<Paint> {
let style = biased(&self.styles, biased_style)?;
let (rgb, mut alpha) = if style.is_material {
let mut m = biased(&self.materials, style.index)?;
if let Material::Textured { base } = *m {
m = biased(&self.materials, base)?;
}
match *m {
Material::Plain { diffuse, alpha } => (self.colour(diffuse)?, alpha),
Material::Textured { .. } => return None,
}
} else {
(self.colour(style.index)?, 1.0)
};
let mut alpha_unset = false;
match style.transparency {
Some(t) => {
let t = f64::from(t) / 255.0;
alpha = match rule {
StyleAlpha::ZeroUnset | StyleAlpha::StyleWins => t,
StyleAlpha::Multiply => alpha * t,
};
}
None if rule == StyleAlpha::ZeroUnset && style.is_material && alpha == 0.0 => {
alpha = 1.0;
alpha_unset = true;
}
None => {}
}
// A NaN alpha (a stored double may be one) reads as opaque.
let a = if alpha.is_nan() { 1.0 } else { alpha.clamp(0.0, 1.0) };
Some(Paint { rgba: Rgba { a, ..rgb }, alpha_unset })
}
}
/// The style a chain of graphics resolves to, outermost first [WD 7.2.4.2]: a son's own
/// style is used unless an ancestor set `FatherHeritColor` (the oldest such wins), and a son
/// setting `SonHeritColor` overrides that. Entities with no style inherit. Returns the
/// winning entity's graphics, style 0 when none.
pub(crate) fn resolve_style(chain: &[Graphics]) -> Graphics {
let (mut won, mut forced) = (Graphics::default(), false);
for g in chain {
if g.style == 0 {
continue;
}
if !forced || g.bits & SON_HERIT_COLOR != 0 {
won = *g;
forced = g.bits & FATHER_HERIT_COLOR != 0;
}
}
won
}
impl Ctx<'_, '_> {
/// `FileStructureGlobals` (303) [WD 7.3.5]: colours, materials, styles and the reference
/// coordinate systems. Pictures, textures and line patterns are read past; fonts and fill
/// patterns are [`PrcError::Unsupported`].
// prc-rs: the globals' field order.
pub(crate) fn globals(&mut self) -> Result<Globals, PrcError> {
let mut g = Globals::default();
self.expect_type(FILE_STRUCTURE_GLOBALS)?;
self.content_prc_base()?;
let n = self.count(4, "referenced file structures")?;
for _ in 0..n {
self.unique_id()?;
}
self.r.double()?; // tessellation chord-height ratio
self.r.double()?; // tessellation angle
self.r.string()?; // default font family
if self.r.unsigned_integer()? != 0 {
return Err(PrcError::Unsupported("PRC global fonts"));
}
let n = self.count(6, "colours")?;
g.colours = (0..n).map(|_| self.vector3().map(|Vec3([r, g, b])| Rgba { r, g, b, a: 1.0 })).collect::<Result<_, _>>()?;
let n = self.count(1, "pictures")?;
for _ in 0..n {
self.picture()?;
}
let n = self.count(1, "texture definitions")?;
for _ in 0..n {
self.texture_definition()?;
}
let n = self.count(1, "materials")?;
g.materials = (0..n).map(|_| self.material()).collect::<Result<_, _>>()?;
let n = self.count(1, "line patterns")?;
for _ in 0..n {
self.line_pattern()?;
}
let n = self.count(1, "styles")?;
g.styles = (0..n).map(|_| self.style()).collect::<Result<_, _>>()?;
if self.r.unsigned_integer()? != 0 {
return Err(PrcError::Unsupported("PRC fill patterns"));
}
let n = self.count(1, "reference coordinate systems")?;
for _ in 0..n {
self.expect_type(RI_COORDINATE_SYSTEM)?;
self.base_with_graphics()?;
self.r.unsigned_integer()?; // local coordinate system + 1
self.r.unsigned_integer()?; // tessellation + 1
g.systems.push(self.transformation()?);
self.schema.skip_added_fields(RI_COORDINATE_SYSTEM, &mut self.r)?;
self.user_data()?;
}
self.schema.skip_added_fields(FILE_STRUCTURE_GLOBALS, &mut self.r)?;
self.user_data()?;
Ok(g)
}
/// `LinePattern` (721) [WD 7.5.2]: read past.
fn line_pattern(&mut self) -> Result<(), PrcError> {
self.expect_type(LINE_PATTERN)?;
self.content_prc_ref_base()?;
self.doubles("line pattern lengths")?;
self.r.double()?; // phase
self.r.bit()?; // is_real_length
self.schema.skip_added_fields(LINE_PATTERN, &mut self.r)
}
/// One `materials` entry, type-tagged: `Material` (702) or `TextureApplication` (711)
/// [WD 7.5.4, 7.5.6].
// prc-rs: a material entry is type-tagged, then a ref base, then its fields.
fn material(&mut self) -> Result<Material, PrcError> {
let t = self.r.unsigned_integer()?;
self.content_prc_ref_base()?;
let m = match t {
MATERIAL => {
self.r.unsigned_integer()?; // ambient + 1
let diffuse = self.r.unsigned_integer()?;
self.r.unsigned_integer()?; // emissive + 1
self.r.unsigned_integer()?; // specular + 1
self.r.double()?; // shininess
self.r.double()?; // ambient alpha
let alpha = self.r.double()?;
self.r.double()?; // emissive alpha
self.r.double()?; // specular alpha
Material::Plain { diffuse, alpha }
}
TEXTURE_APPLICATION => {
let base = self.r.unsigned_integer()?;
self.r.unsigned_integer()?; // texture definition + 1
self.r.unsigned_integer()?; // next texture + 1
self.r.unsigned_integer()?; // UV coordinates set + 1
Material::Textured { base }
}
t => return Err(malformed(format!("entity type {t} as a material"))),
};
self.schema.skip_added_fields(t, &mut self.r)?;
Ok(m)
}
/// `Style` (701) [WD 7.5.3].
// prc-rs: the transparency and the three rendering parameters each follow a presence bit.
fn style(&mut self) -> Result<Style, PrcError> {
self.expect_type(STYLE)?;
self.content_prc_ref_base()?;
self.r.double()?; // line width
self.r.bit()?; // is_vpicture
self.r.unsigned_integer()?; // line pattern + 1
let is_material = self.r.bit()?;
let index = self.r.unsigned_integer()?;
let transparency = if self.r.bit()? { Some(self.r.character()?) } else { None };
for _ in 0..3 {
if self.r.bit()? {
self.r.character()?;
}
}
self.schema.skip_added_fields(STYLE, &mut self.r)?;
Ok(Style { is_material, index, transparency })
}
/// `Picture` (703) [WD 7.5.5]: read past.
fn picture(&mut self) -> Result<(), PrcError> {
self.expect_type(PICTURE)?;
self.content_prc_base()?;
for _ in 0..4 {
self.r.unsigned_integer()?; // format, file + 1, width, height
}
self.schema.skip_added_fields(PICTURE, &mut self.r)
}
/// `TextureDefinition` (712) [WD 7.5.7; pdf-issues #485]: read past.
fn texture_definition(&mut self) -> Result<(), PrcError> {
self.expect_type(TEXTURE_DEFINITION)?;
self.content_prc_ref_base()?;
self.r.unsigned_integer()?; // picture + 1
let dimension = self.r.character()?;
if self.r.integer()? == TEXTURE_MAPPING_OPERATOR {
self.r.integer()?; // mapping operator
if self.r.bit()? {
self.transformation()?;
}
}
self.r.unsigned_integer()?; // channels
self.doubles("texture intensities")?;
let n = self.count(8, "texture components")?;
for _ in 0..n {
self.r.character()?;
}
if self.r.integer()? == TEXTURE_FUNCTION_BLEND {
for _ in 0..4 {
self.r.double()?; // blend colour
}
}
for _ in 0..2 {
// RGB, then alpha: a source blend, and a destination one when set.
if self.r.integer()? != 0 {
self.r.integer()?;
}
}
if self.r.character()? & TEXTURE_ALPHA_TEST != 0 {
self.r.integer()?; // alpha test function
self.r.double()?; // reference
}
for _ in 0..dimension.clamp(1, 3) {
self.r.integer()?; // wrapping mode
}
if self.r.bit()? {
self.texture_transformation()?;
}
self.schema.skip_added_fields(TEXTURE_DEFINITION, &mut self.r)
}
/// `TextureTransformation` (713) [WD 7.5.8, 7.4.11.1; pdf-issues #819]: read past.
fn texture_transformation(&mut self) -> Result<(), PrcError> {
self.expect_type(TEXTURE_TRANSFORMATION)?;
self.r.bit()?; // invert u
self.r.bit()?; // invert v
self.r.bit()?; // is 2D
let b = self.r.character()?;
// Translation; non-orthogonal axes, else one rotation vector; non-uniform, else
// uniform scale; homogeneous row.
let doubles = [(0x01, 2), (0x20, 4), (0x10, 2), (0x40, 3)].iter().filter(|(bit, _)| b & bit != 0).map(|(_, n)| n).sum::<usize>()
+ if b & 0x20 == 0 && b & 0x02 != 0 { 2 } else { 0 }
+ usize::from(b & 0x10 == 0 && b & 0x08 != 0);
for _ in 0..doubles {
self.r.double()?;
}
self.schema.skip_added_fields(TEXTURE_TRANSFORMATION, &mut self.r)
}
}
+576
View File
@@ -0,0 +1,576 @@
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::style::{Material, Style};
use super::{EntityOverrides, FATHER_HERIT_COLOR, Globals, Graphics, MAX_PLACEMENTS, NameSource, SHOW, SON_HERIT_COLOR, StyleAlpha, resolve_style};
use crate::bits::BitReader;
use crate::entity::Ctx;
use crate::tess::tests::two_face_square;
use crate::testw::W;
use crate::{PrcError, PrcFile, Rgba, Schema, Tessellation, TriangleMesh, UniqueId};
const RED: Rgba = Rgba { r: 1., g: 0., b: 0., a: 1. };
const GREEN: Rgba = Rgba { r: 0., g: 1., b: 0., a: 1. };
const BLUE: Rgba = Rgba { r: 0., g: 0., b: 1., a: 1. };
/// Visible graphics with no style.
const PLAIN: (u32, u16) = (0, SHOW);
/// The biased style indices of [`palette`]: red, green, a blue material.
const S_RED: u32 = 1;
const S_GREEN: u32 = 2;
const S_BLUE: u32 = 3;
const RI_POLY_BREP: u32 = 237;
fn fs_id(i: usize) -> UniqueId {
UniqueId([0x100 + u32::try_from(i).unwrap(), 1, 2, 3])
}
fn len(n: usize) -> u32 {
u32::try_from(n).unwrap()
}
/// `ContentPRCBase` with no attributes and its own name.
fn base(w: &mut W, name: Option<&str>) {
w.uint(0).bit(false).string(name);
}
fn ref_base(w: &mut W, name: Option<&str>, uid: u32) {
base(w, name);
w.uint(0).uint(0).uint(uid);
}
/// `PRCBaseWithGraphics` with its own `(style, behaviour)`.
fn graphics(w: &mut W, name: Option<&str>, uid: u32, (style, bits): (u32, u16)) {
ref_base(w, name, uid);
w.bit(false).uint(0).uint(style).put(u64::from(bits & 0xff), 8).put(u64::from(bits >> 8), 8);
}
fn file_id(w: &mut W, fs: Option<usize>) {
match fs {
None => {
w.bit(true);
}
Some(i) => {
w.bit(false);
for v in fs_id(i).0 {
w.uint(v);
}
}
}
}
/// An `RI_PolyBrepModel` drawing biased tessellation `tess` under local system `cs`.
fn poly_item(w: &mut W, uid: u32, cs: u32, tess: u32, g: (u32, u16)) {
w.uint(RI_POLY_BREP);
graphics(w, None, uid, g);
w.uint(cs).uint(tess).bit(false).uint(0);
}
/// An `RI_BrepModel` drawing tessellation 1, its exact body stored as `brep`.
fn brep_item(w: &mut W, uid: u32, g: (u32, u16), brep: (u32, u32)) {
w.uint(232);
graphics(w, None, uid, g);
w.uint(0).uint(1).bit(true).uint(brep.0).uint(brep.1).bit(false).uint(0);
}
/// A `PartDefinition` holding the `n` items `items` writes.
fn part(name: Option<&str>, g: (u32, u16), n: usize, items: impl FnOnce(&mut W)) -> W {
let mut w = W::default();
w.uint(311);
graphics(&mut w, name, 0, g);
for _ in 0..6 {
w.double(0.);
}
w.uint(len(n));
items(&mut w);
for _ in 0..6 {
w.uint(0); // markups, views, user data
}
w
}
/// One poly-B-rep item (uid 7) drawing tessellation 1 in style `style`.
fn plate(style: u32) -> W {
part(Some("Plate"), PLAIN, 1, |w| poly_item(w, 7, 0, 1, (style, SHOW)))
}
#[derive(Default)]
struct Occ<'a> {
name: Option<&'a str>,
g: Option<(u32, u16)>,
part: u32,
prototype: u32,
prototype_fs: Option<usize>,
sons: Vec<u32>,
suppressed: bool,
location: Option<W>,
refs: Vec<W>,
}
fn occ(w: &mut W, o: &Occ<'_>) {
w.uint(310);
graphics(w, o.name, 0, o.g.unwrap_or(PLAIN));
w.uint(o.part).uint(o.prototype);
if o.prototype != 0 {
file_id(w, o.prototype_fs);
}
w.uint(0).uint(len(o.sons.len()));
for &s in &o.sons {
w.uint(s);
}
w.put(u64::from(o.suppressed), 8);
w.bit(false).double(1.).put(0, 8).int(0);
match &o.location {
Some(t) => w.bit(true).append(t),
None => w.bit(false),
};
w.uint(len(o.refs.len()));
for r in &o.refs {
w.append(r);
}
for _ in 0..5 {
w.uint(0); // markups, views
}
w.bit(false).uint(0).uint(0).uint(0);
}
fn tree(parts: &[W], occs: &[Occ<'_>]) -> W {
let mut w = W::default();
w.uint(304);
base(&mut w, None);
w.uint(len(parts.len()));
for p in parts {
w.append(p);
}
w.uint(len(occs.len()));
for o in occs {
occ(&mut w, o);
}
w.uint(302);
base(&mut w, None);
w.uint(0).uint(0).uint(0);
w
}
fn style(index: u32, is_material: bool, transparency: Option<u8>) -> W {
let mut w = W::default();
w.uint(701);
ref_base(&mut w, None, 0);
w.double(0.).bit(false).uint(0).bit(is_material).uint(index);
match transparency {
Some(t) => w.bit(true).put(u64::from(t), 8),
None => w.bit(false),
};
w.bit(false).bit(false).bit(false);
w
}
fn material(diffuse: u32, alpha: f64) -> W {
let mut w = W::default();
w.uint(702);
ref_base(&mut w, None, 0);
w.uint(0).uint(diffuse).uint(0).uint(0);
w.double(0.).double(1.).double(alpha).double(1.).double(1.);
w
}
/// A globals section (schema included) with `colours`, `materials`, `styles` and reference
/// coordinate systems `systems` (transformations).
fn globals(colours: &[Rgba], materials: &[W], styles: &[W], systems: &[W]) -> W {
let mut w = W::default();
w.uint(0).uint(303);
base(&mut w, None);
w.uint(0).double(0.).double(0.).string(None).uint(0);
w.uint(len(colours.len()));
for c in colours {
w.double(c.r).double(c.g).double(c.b);
}
w.uint(0).uint(0).uint(len(materials.len()));
for m in materials {
w.append(m);
}
w.uint(0).uint(len(styles.len()));
for s in styles {
w.append(s);
}
w.uint(0).uint(len(systems.len()));
for t in systems {
w.uint(240);
graphics(&mut w, None, 0, PLAIN);
w.uint(0).uint(0).append(t).uint(0);
}
w.uint(0);
w
}
/// Red, green and blue colours, and styles [`S_RED`], [`S_GREEN`] and [`S_BLUE`] (a
/// material with diffuse alpha 0).
fn palette() -> W {
globals(&[RED, GREEN, BLUE], &[material(7, 0.)], &[style(1, false, None), style(4, false, None), style(1, true, None)], &[])
}
fn translate(t: [f64; 3]) -> W {
let mut w = W::default();
w.uint(202).put(0x01, 8).double(t[0]).double(t[1]).double(t[2]);
w
}
/// A translation by `t` after a uniform scale by `s`.
fn translate_scale(t: [f64; 3], s: f64) -> W {
let mut w = W::default();
w.uint(202).put(0x01 | 0x08, 8).double(t[0]).double(t[1]).double(t[2]).double(s);
w
}
/// An entity reference giving the item with PRC unique id `uid` the graphics `g`.
fn ref_entity(uid: u32, g: (u32, u16)) -> W {
let mut w = W::default();
w.uint(203);
graphics(&mut w, None, 0, g);
w.uint(0).bit(true).uint(205).uint(RI_POLY_BREP).bit(true).uint(uid).uint(0);
w
}
/// An entity reference giving faces `faces` of B-rep `brep` the graphics `g`.
fn ref_faces(brep: (u32, u32), faces: &[u32], g: (u32, u16)) -> W {
let mut w = W::default();
w.uint(203);
graphics(&mut w, None, 0, g);
w.uint(0).bit(true).uint(206).uint(149).bit(true).bit(true).uint(brep.0).uint(brep.1).uint(len(faces.len()));
for &f in faces {
w.uint(f);
}
w.uint(0);
w
}
/// A model file (schema included) whose roots are `(file structure, biased occurrence)`.
fn model(roots: &[(usize, u32)]) -> W {
let mut w = W::default();
w.uint(0).uint(301);
base(&mut w, None);
w.bit(false).double(1.).uint(len(roots.len()));
for &(fs, index) in roots {
for v in fs_id(fs).0 {
w.uint(v);
}
w.uint(index).bit(true);
}
w
}
/// One file structure's globals, tree and tessellation sections.
struct Fs {
globals: W,
tree: W,
tess: W,
}
fn zlib(data: &[u8]) -> Vec<u8> {
use std::io::Write as _;
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(data).unwrap();
e.finish().unwrap()
}
fn u32s(out: &mut Vec<u8>, vs: &[u32]) {
vs.iter().for_each(|v| out.extend_from_slice(&v.to_le_bytes()));
}
/// A PRC stream of `structures` and `model`.
fn prc(structures: &[Fs], model: &W) -> Vec<u8> {
let header_len = 59 + 48 * structures.len();
let at = |body: &Vec<u8>| len(header_len + body.len());
let mut body = Vec::new();
let mut descriptions = Vec::new();
for (i, fs) in structures.iter().enumerate() {
let mut offs = vec![at(&body)];
body.extend(b"PRC");
u32s(&mut body, &[8137, 8137]);
u32s(&mut body, &fs_id(i).0);
u32s(&mut body, &[0, 0, 0, 0, 0]);
for s in [fs.globals.bytes(), fs.tree.bytes(), fs.tess.bytes(), Vec::new(), Vec::new()] {
offs.push(at(&body));
body.extend(zlib(&s));
}
descriptions.push((fs_id(i), offs));
}
let mf_start = at(&body);
body.extend(zlib(&model.bytes()));
let mf_end = at(&body);
let mut out = b"PRC".to_vec();
u32s(&mut out, &[8137, 8137, 1, 2, 3, 4, 0, 0, 0, 0, len(structures.len())]);
for (id, offs) in descriptions {
u32s(&mut out, &id.0);
u32s(&mut out, &[0, 6]);
u32s(&mut out, &offs);
}
u32s(&mut out, &[mf_start, mf_end, 0]);
out.extend(body);
assert_eq!(out.len(), usize::try_from(mf_end).unwrap());
out
}
/// One file structure with [`palette`], `parts` and `occs`, whose root is occurrence 0.
fn single(parts: &[W], occs: &[Occ<'_>]) -> PrcFile {
PrcFile::parse(&prc(&[Fs { globals: palette(), tree: tree(parts, occs), tess: two_face_square() }], &model(&[(0, 1)]))).unwrap()
}
fn mesh(f: &PrcFile, fs: usize) -> TriangleMesh {
match f.file_structures[fs].tessellations().unwrap().into_iter().next() {
Some(Tessellation::Mesh(m)) => m,
other => panic!("not a mesh: {other:?}"),
}
}
fn colours(f: &PrcFile, scope: EntityOverrides) -> Vec<Option<Rgba>> {
f.placements_with(StyleAlpha::default(), scope).unwrap().iter().map(|p| p.colour).collect()
}
fn malformed(r: Result<impl std::fmt::Debug, PrcError>) -> String {
match r {
Err(PrcError::Malformed(m)) => m,
other => panic!("expected Malformed, got {other:?}"),
}
}
#[test]
fn a_parsed_assembly_places_names_and_colours_each_occurrence() {
let occs = [
Occ { name: Some("Asm"), sons: vec![1, 2], location: Some(translate([0., 0., 100.])), ..Occ::default() },
Occ { part: 1, location: Some(translate([10., 0., 0.])), ..Occ::default() },
Occ { name: Some("B"), part: 1, location: Some(translate([0., 5., 0.])), ..Occ::default() },
];
let f = single(&[plate(S_RED)], &occs);
assert!(f.stores_tree());
let p = f.placements().unwrap();
assert_eq!(p.len(), 2);
let m = mesh(&f, 0);
let moved: Vec<_> = p.iter().map(|p| (p.file_structure, p.tessellation, m.transformed(&p.matrix).positions[2])).collect();
assert_eq!(moved, [(0, 0, [11., 1., 100.]), (0, 0, [1., 6., 100.])]);
assert_eq!(p.iter().map(|p| p.colour).collect::<Vec<_>>(), [Some(RED); 2]);
// A material with diffuse alpha 0 and no style transparency reads as opaque.
assert_eq!(colours(&single(&[plate(S_BLUE)], &occs), EntityOverrides::default()), [Some(BLUE); 2]);
let nodes = f.model_tree().unwrap();
let summary: Vec<_> = nodes.iter().map(|n| (n.name.as_deref(), n.name_from, n.parent, n.depth, n.placements.clone())).collect();
assert_eq!(
summary,
[
(Some("Asm"), NameSource::Occurrence, None, 0, 0..2),
(Some("Plate"), NameSource::Part, Some(0), 1, 0..1),
(Some("B"), NameSource::Occurrence, Some(0), 1, 1..2),
]
);
}
#[test]
fn a_file_without_a_model_file_entity_has_no_tree() {
let fs = Fs { globals: palette(), tree: tree(&[], &[]), tess: two_face_square() };
let bytes = prc(&[fs], &W::default());
let f = PrcFile::parse(&bytes).unwrap();
assert!(!f.stores_tree());
assert!(f.placements().unwrap().is_empty());
}
#[test]
fn an_occurrence_that_contains_itself_is_an_error() {
let occs = [Occ { sons: vec![1], ..Occ::default() }, Occ { part: 1, sons: vec![0, 0], ..Occ::default() }];
assert!(malformed(single(&[plate(S_RED)], &occs).placements()).contains("contains itself"));
}
#[test]
fn a_prototype_cycle_is_an_error() {
let occs = [Occ { prototype: 2, ..Occ::default() }, Occ { prototype: 1, ..Occ::default() }];
assert!(malformed(single(&[], &occs).model_tree()).contains("own prototype"));
}
#[test]
fn an_exponential_walk_is_cut_off() {
// Occurrence k has occurrence k + 1 twice as sons: 2^21 visits.
let occs: Vec<_> = (0..21).map(|k| Occ { sons: if k < 20 { vec![k + 1, k + 1] } else { Vec::new() }, ..Occ::default() }).collect();
assert!(malformed(single(&[], &occs).model_tree()).contains("too large"));
}
#[test]
fn placements_past_the_ceiling_are_an_error() {
// 1,025 placements of a part with 1,024 items.
let items = 1024;
let wide = part(None, PLAIN, items, |w| (0..items).for_each(|_| poly_item(w, 0, 0, 1, PLAIN)));
let occs = [Occ { sons: vec![1; 1025], ..Occ::default() }, Occ { part: 1, ..Occ::default() }];
assert!(1025 * items > MAX_PLACEMENTS);
assert!(malformed(single(&[wide], &occs).placements()).contains("placements"));
}
#[test]
fn hidden_and_suppressed_occurrences_are_listed_but_not_placed() {
let occs = [
Occ { sons: vec![1, 2, 3], ..Occ::default() },
Occ { part: 1, ..Occ::default() },
Occ { part: 1, suppressed: true, ..Occ::default() },
Occ { part: 1, sons: vec![1], g: Some((0, 0)), ..Occ::default() },
];
let f = single(&[plate(S_RED)], &occs);
assert_eq!(f.placements().unwrap().len(), 1);
let nodes = f.model_tree().unwrap();
let flags: Vec<_> = nodes.iter().map(|n| (n.occurrence, n.hidden, n.suppressed, n.drawn)).collect();
assert_eq!(flags, [(0, false, false, true), (1, false, false, true), (2, false, true, false), (3, true, false, false), (1, false, false, false)]);
}
#[test]
fn a_father_herit_colour_beats_the_part_and_a_son_herit_colour_beats_that() {
let forced = (S_GREEN, SHOW | FATHER_HERIT_COLOR);
let occs = [Occ { sons: vec![1], g: Some(forced), ..Occ::default() }, Occ { part: 1, ..Occ::default() }];
assert_eq!(colours(&single(&[plate(S_RED)], &occs), EntityOverrides::default()), [Some(GREEN)]);
let son = part(None, PLAIN, 1, |w| poly_item(w, 7, 0, 1, (S_RED, SHOW | SON_HERIT_COLOR)));
assert_eq!(colours(&single(&[son], &occs), EntityOverrides::default()), [Some(RED)]);
// Unforced, the son's own style wins over its father's.
let occs = [Occ { sons: vec![1], g: Some((S_GREEN, SHOW)), ..Occ::default() }, Occ { part: 1, ..Occ::default() }];
assert_eq!(colours(&single(&[plate(S_RED)], &occs), EntityOverrides::default()), [Some(RED)]);
}
#[test]
fn resolve_style_takes_the_oldest_forcing_ancestor() {
let g = |style, bits| Graphics { style, bits, fs: 0 };
let chain = [g(1, FATHER_HERIT_COLOR), g(2, FATHER_HERIT_COLOR), g(0, 0), g(3, 0)];
assert_eq!(resolve_style(&chain).style, 1);
assert_eq!(resolve_style(&[g(1, 0), g(0, 0)]).style, 1);
assert_eq!(resolve_style(&[]).style, 0);
}
#[test]
fn a_prototypes_part_is_coloured_by_its_own_structures_styles() {
// Structure 1 is a part library whose style 1 is green; structure 0's style 1 is red.
let library = Fs {
globals: globals(&[GREEN], &[], &[style(1, false, None)], &[]),
tree: tree(&[plate(1)], &[Occ { name: Some("Bolt"), part: 1, ..Occ::default() }]),
tess: two_face_square(),
};
let assembly = Fs {
globals: globals(&[RED], &[], &[style(1, false, None)], &[]),
tree: tree(&[], &[Occ { prototype: 1, prototype_fs: Some(1), location: Some(translate([1., 2., 3.])), ..Occ::default() }]),
tess: W::default(),
};
let f = PrcFile::parse(&prc(&[assembly, library], &model(&[(0, 1)]))).unwrap();
let p = f.placements().unwrap();
assert_eq!(p.iter().map(|p| (p.file_structure, p.colour, p.matrix.at(1, 3))).collect::<Vec<_>>(), [(1, Some(GREEN), 2.)]);
let n = f.model_tree().unwrap();
assert_eq!((n[0].name.as_deref(), n[0].name_from), (Some("Bolt"), NameSource::Prototype));
}
/// A root placing the red plate twice, the first occurrence carrying `refs`.
fn shared_plate(refs: Vec<W>) -> PrcFile {
let occs = [Occ { sons: vec![1, 2], ..Occ::default() }, Occ { part: 1, refs, ..Occ::default() }, Occ { part: 1, ..Occ::default() }];
single(&[plate(S_RED)], &occs)
}
#[test]
fn an_entity_reference_reaches_its_scope() {
let f = shared_plate(vec![ref_entity(7, (S_GREEN, SHOW))]);
assert_eq!(colours(&f, EntityOverrides::Subtree), [Some(GREEN), Some(RED)]);
assert_eq!(colours(&f, EntityOverrides::Everywhere), [Some(GREEN), Some(GREEN)]);
assert_eq!(colours(&f, EntityOverrides::Ignore), [Some(RED), Some(RED)]);
// A reference to another uid changes nothing; one without Show hides its target.
assert_eq!(colours(&shared_plate(vec![ref_entity(8, (S_GREEN, SHOW))]), EntityOverrides::Subtree), [Some(RED); 2]);
assert_eq!(colours(&shared_plate(vec![ref_entity(7, (0, 0))]), EntityOverrides::Subtree), [Some(RED)]);
}
#[test]
fn a_topology_reference_colours_its_faces() {
let brep = part(None, PLAIN, 1, |w| brep_item(w, 7, (S_RED, SHOW), (4, 9)));
let occs = [Occ { part: 1, refs: vec![ref_faces((4, 9), &[1], (S_GREEN, SHOW))], ..Occ::default() }];
let f = single(&[brep], &occs);
let p = f.placements().unwrap();
assert_eq!(p[0].colour, Some(RED));
assert_eq!(p[0].triangle_colours(&mesh(&f, 0)), Some(vec![Some(RED), Some(GREEN)]));
// Another body's faces are left alone.
let occs = [Occ { part: 1, refs: vec![ref_faces((4, 8), &[1], (S_GREEN, SHOW))], ..Occ::default() }];
let f = single(&[part(None, PLAIN, 1, |w| brep_item(w, 7, (S_RED, SHOW), (4, 9)))], &occs);
assert_eq!(f.placements().unwrap()[0].triangle_colours(&mesh(&f, 0)), None);
}
#[test]
fn an_items_coordinate_system_applies_after_the_occurrence_location() {
let fs = Fs {
globals: globals(&[RED], &[], &[], &[translate([0., 0., 7.])]),
tree: tree(
&[part(None, PLAIN, 1, |w| poly_item(w, 7, 1, 1, PLAIN))],
&[Occ { part: 1, location: Some(translate_scale([1., 0., 0.], 2.)), ..Occ::default() }],
),
tess: two_face_square(),
};
let f = PrcFile::parse(&prc(&[fs], &model(&[(0, 1)]))).unwrap();
let p = f.placements().unwrap();
assert_eq!((p[0].matrix.at(0, 3), p[0].matrix.at(2, 3), p[0].colour), (1., 14., None));
}
#[test]
fn a_colour_index_counts_doubles() {
// pdf-issues #816: colour i is stored as 3i + 1.
let g =
|index| Globals { colours: vec![RED, GREEN], styles: vec![Style { is_material: false, index, transparency: None }], ..Globals::default() };
let paint = |index| g(index).style_paint(1, StyleAlpha::default()).map(|p| p.rgba);
assert_eq!(paint(1), Some(RED));
assert_eq!(paint(4), Some(GREEN));
assert_eq!(paint(2), None);
assert_eq!(paint(7), None);
}
#[test]
fn style_alpha_rules_combine_transparency_with_material_alpha() {
let g = |alpha, transparency| Globals {
colours: vec![BLUE],
materials: vec![Material::Plain { diffuse: 1, alpha }],
styles: vec![Style { is_material: true, index: 1, transparency }],
..Globals::default()
};
let a = |alpha, t, rule| g(alpha, t).style_paint(1, rule).unwrap().rgba.a;
assert_eq!(a(0., None, StyleAlpha::ZeroUnset), 1.);
assert_eq!(a(0., None, StyleAlpha::StyleWins), 0.);
assert_eq!(a(0.5, Some(51), StyleAlpha::StyleWins), 0.2);
assert_eq!(a(0.5, Some(51), StyleAlpha::Multiply), 0.1);
assert_eq!(a(f64::NAN, None, StyleAlpha::Multiply), 1.);
// A textured material takes its base's colour.
let mut t = g(1., None);
t.materials.push(Material::Textured { base: 1 });
t.styles[0].index = 2;
assert_eq!(t.style_paint(1, StyleAlpha::default()).unwrap().rgba, BLUE);
}
fn transformation(behaviour: u8, fields: &[f64]) -> super::Matrix {
let mut w = W::default();
w.uint(202).put(u64::from(behaviour), 8);
for &d in fields {
w.double(d);
}
let bytes = w.bytes();
Ctx::new(BitReader::new(&bytes), &Schema::default(), 8137).transformation().unwrap()
}
#[test]
fn a_mirrored_rotation_takes_y_cross_x_and_both_scales_multiply() {
let x_y = [1., 0., 0., 0., 1., 0.];
assert_eq!(transformation(0x02, &x_y).at(2, 2), 1.);
assert_eq!(transformation(0x02 | 0x04, &x_y).at(2, 2), -1.);
let m = transformation(0x10 | 0x08, &[1., 2., 3., 2.]);
assert_eq!([m.at(0, 0), m.at(1, 1), m.at(2, 2)], [2., 4., 6.]);
let m = transformation(0x40, &[0., 0., 0.5, 2.]);
assert_eq!(m.0[3], [0., 0., 0.5, 2.]);
}
#[test]
fn every_truncation_of_a_tree_or_globals_is_an_error_not_a_panic() {
let occs = [
Occ { name: Some("Asm"), sons: vec![1], location: Some(translate([0., 0., 1.])), ..Occ::default() },
Occ { part: 1, refs: vec![ref_entity(7, (S_GREEN, SHOW)), ref_faces((1, 1), &[0], PLAIN)], ..Occ::default() },
];
let t = tree(&[plate(S_RED)], &occs).bytes();
let schema = Schema::default();
for end in 0..t.len().saturating_sub(1) {
assert!(Ctx::new(BitReader::new(&t[..end]), &schema, 8137).file_structure_tree().is_err(), "tree cut at {end}");
}
let g = palette().bytes();
for end in 1..g.len().saturating_sub(1) {
let mut r = BitReader::new(&g[..end]);
if Schema::read(&mut r).is_ok() {
assert!(Ctx::new(r, &schema, 8137).globals().is_err(), "globals cut at {end}");
}
}
}
+262
View File
@@ -0,0 +1,262 @@
//! The occurrence walk that turns the trees into placements [WD 7.3.10.1, 7.6.3.2].
use super::overrides::{Active, Reached};
use super::{
EntityOverrides, Globals, Graphics, MAX_DEPTH, MAX_PLACEMENTS, MAX_VISITS, Matrix, ModelNode, NameSource, Override, Palettes, Placement, Product,
StyleAlpha, Target, Tree, UniqueId, paint_of, resolve_style, style,
};
use crate::PrcError;
use crate::container::offset;
use crate::entity::malformed;
/// Ceiling on face-override entries all placements of one walk carry together.
const MAX_FACE_OVERRIDES: usize = 1 << 24;
/// Where an occurrence sits: its depth, its parent node and whether its ancestors are drawn.
#[derive(Debug, Clone, Copy)]
pub(super) struct At {
depth: usize,
parent: Option<usize>,
drawn: bool,
}
impl At {
/// A root occurrence.
pub(super) const ROOT: At = At { depth: 0, parent: None, drawn: true };
}
/// An occurrence's part and sons as `(file structure, value)`, after the prototype fallback.
struct Resolved<'t> {
part: (usize, u32),
sons: (usize, &'t [u32]),
proto_name: Option<&'t str>,
/// The entity references in force: the occurrence's own, else its prototype chain's
/// first.
overrides: (usize, &'t [Override]),
}
/// The occurrence walk over every file structure's tree.
pub(super) struct Walk<'t> {
/// Per file structure: its id, tree and globals.
trees: Vec<(UniqueId, &'t Tree, &'t Globals)>,
pub(super) out: Vec<Placement>,
pub(super) nodes: Vec<ModelNode>,
visits: usize,
face_overrides: usize,
palettes: Palettes,
scope: EntityOverrides,
/// The entity references of the occurrences being walked, outermost first; under
/// [`EntityOverrides::Everywhere`], every occurrence's.
active: Vec<Active<'t>>,
/// The `(file structure, occurrence)` of each occurrence being walked, root first: an
/// occurrence met again on its own path is a cycle.
path: Vec<(usize, usize)>,
}
impl<'t> Walk<'t> {
/// A walk over `trees`, with no placements yet, colouring by `rule` and applying entity
/// references by `scope`.
pub(super) fn new(trees: Vec<(UniqueId, &'t Tree, &'t Globals)>, rule: StyleAlpha, scope: EntityOverrides) -> Self {
let palettes = trees
.iter()
.map(|(_, _, g)| (0..=g.styles.len()).map(|b| u32::try_from(b).ok().and_then(|b| g.style_paint(b, rule))).collect())
.collect();
let mut walk =
Walk { trees, out: Vec::new(), nodes: Vec::new(), visits: 0, face_overrides: 0, palettes, scope, active: Vec::new(), path: Vec::new() };
if scope == EntityOverrides::Everywhere {
let trees = walk.trees.clone();
for (fs, t) in trees.iter().enumerate() {
for p in &t.1.products {
walk.activate(fs, &p.overrides);
}
}
}
walk
}
/// Puts `overrides`, held by an occurrence of structure `owner`, in force. One naming an
/// unknown structure is left out.
fn activate(&mut self, owner: usize, overrides: &'t [Override]) {
for ov in overrides {
let named = match (ov.target.fs(), &ov.target) {
(Some(id), _) => match self.fs(id) {
Ok(n) => Some(n),
Err(_) => continue,
},
(None, Target::Entity { .. }) => Some(owner),
(None, Target::Topology { .. }) => None,
};
self.active.push((owner, named, ov));
}
}
fn fs(&self, id: UniqueId) -> Result<usize, PrcError> {
self.trees.iter().position(|t| t.0 == id).ok_or_else(|| malformed("reference to an unknown file structure".into()))
}
fn product(&self, fs: usize, index: usize) -> Result<&'t Product, PrcError> {
self.trees.get(fs).and_then(|t| t.1.products.get(index)).ok_or_else(|| malformed(format!("product occurrence {index} does not exist")))
}
/// The structure a file identifier names: `id`, or `fs` itself when it names none.
fn named(&self, id: Option<UniqueId>, fs: usize) -> Result<usize, PrcError> {
id.map_or(Ok(fs), |id| self.fs(id))
}
/// Walks occurrence `index` of structure `fs` under a father placed at `father`, whose
/// occurrences' graphics are `graphics` (root first), and lists it as a node. A hidden or
/// suppressed subtree is listed with `drawn` false and placed nowhere.
pub(super) fn occurrence(&mut self, fs: usize, index: usize, father: &Matrix, graphics: &[Graphics], at: At) -> Result<(), PrcError> {
self.visits = self.visits.saturating_add(1);
if at.depth > MAX_DEPTH || self.visits > MAX_VISITS {
return Err(malformed("the occurrence tree is too deep or too large".into()));
}
if self.path.contains(&(fs, index)) {
return Err(malformed(format!("product occurrence {index} contains itself")));
}
let p = self.product(fs, index)?;
let m = p.location.map_or(*father, |l| father.multiply(&l));
let mut chain = graphics.to_vec();
chain.push(Graphics { fs, ..p.graphics });
let r = self.resolve(fs, index, p)?;
let drawn = at.drawn && !p.hidden && !p.suppressed;
let in_force = self.active.len();
if self.scope == EntityOverrides::Subtree {
self.activate(r.overrides.0, r.overrides.1);
}
let node = self.nodes.len();
let first = self.out.len();
let (name, name_from) = self.name_of(p, &r);
self.nodes.push(ModelNode {
name,
name_from,
parent: at.parent,
depth: at.depth,
file_structure: fs,
occurrence: index,
hidden: p.hidden,
suppressed: p.suppressed,
drawn,
has_part: r.part.1 != 0,
placements: first..first,
});
if let (true, Some(part)) = (drawn, r.part.1.checked_sub(1)) {
self.part(r.part.0, offset(part), &m, &chain)?;
}
// Son indices are 0-based; the external one is biased.
let external = match p.external.checked_sub(1) {
Some(e) => Some((self.named(p.external_fs, fs)?, e)),
None => None,
};
let child = At { depth: at.depth + 1, parent: Some(node), drawn };
self.path.push((fs, index));
let walked =
r.sons.1.iter().map(|&s| (r.sons.0, s)).chain(external).try_for_each(|(sfs, s)| self.occurrence(sfs, offset(s), &m, &chain, child));
self.path.pop();
walked?;
self.active.truncate(in_force);
let end = self.out.len();
if let Some(n) = self.nodes.get_mut(node) {
n.placements = first..end;
}
Ok(())
}
/// The part and sons of `p` (occurrence `index` of `fs`), falling back to its prototype
/// chain's for whichever it leaves empty [WD 7.3.10.1], and the first name on that chain.
fn resolve(&self, fs: usize, index: usize, p: &'t Product) -> Result<Resolved<'t>, PrcError> {
let (mut part, mut sons) = ((fs, p.part), (fs, p.sons.as_slice()));
let mut proto_name = None;
let mut overrides = (fs, p.overrides.as_slice());
let mut proto = (fs, p.prototype, p.prototype_fs);
let mut seen = vec![(fs, index)];
while proto.1 != 0 && (part.1 == 0 || sons.1.is_empty() || proto_name.is_none() || overrides.1.is_empty()) {
let pfs = self.named(proto.2, proto.0)?;
let pi = offset(proto.1.saturating_sub(1));
if seen.contains(&(pfs, pi)) {
return Err(malformed(format!("product occurrence {pi} is its own prototype")));
}
if seen.len() > MAX_DEPTH {
return Err(malformed("prototype chain too long".into()));
}
seen.push((pfs, pi));
let q = self.product(pfs, pi)?;
if part.1 == 0 {
part = (pfs, q.part);
}
if sons.1.is_empty() {
sons = (pfs, q.sons.as_slice());
}
if proto_name.is_none() {
proto_name = q.name.as_deref();
}
if overrides.1.is_empty() {
overrides = (pfs, q.overrides.as_slice());
}
proto = (pfs, q.prototype, q.prototype_fs);
}
Ok(Resolved { part, sons, proto_name, overrides })
}
/// The display name: the occurrence's, its prototype chain's, then its part's.
fn name_of(&self, p: &Product, r: &Resolved<'_>) -> (Option<String>, NameSource) {
if let Some(n) = &p.name {
return (Some(n.clone()), NameSource::Occurrence);
}
if let Some(n) = r.proto_name {
return (Some(n.to_owned()), NameSource::Prototype);
}
let part = r.part.1.checked_sub(1).and_then(|i| self.trees.get(r.part.0)?.1.part_names.get(offset(i))).and_then(Option::as_ref);
match part {
Some(n) => (Some(n.clone()), NameSource::Part),
None => (None, NameSource::Unnamed),
}
}
/// Places part definition `index` of structure `fs` at `m`, under `graphics`.
fn part(&mut self, fs: usize, index: usize, m: &Matrix, graphics: &[Graphics]) -> Result<(), PrcError> {
let (_, tree, _) = self.trees.get(fs).copied().ok_or_else(|| malformed("file structure out of range".into()))?;
let items = tree.parts.get(index).ok_or_else(|| malformed(format!("part definition {index} does not exist")))?;
for item in items {
let reached = Reached::collect(&self.active, fs, item);
if reached.hidden {
continue;
}
if self.out.len() >= MAX_PLACEMENTS {
return Err(malformed(format!("more than {MAX_PLACEMENTS} placements")));
}
self.face_overrides = self.face_overrides.saturating_add(reached.faces.len());
if self.face_overrides > MAX_FACE_OVERRIDES {
return Err(malformed("too many face overrides".into()));
}
let matrix = match reached.local {
Some((owner, cs)) => self.located(m, owner, &[cs])?,
None => self.located(m, fs, &item.local)?,
};
let chain: Vec<Graphics> = graphics.iter().copied().chain(item.graphics.iter().map(|&g| Graphics { fs, ..g })).collect();
let won = reached.item.unwrap_or_else(|| resolve_style(&chain));
self.out.push(Placement {
file_structure: fs,
tessellation: offset(item.tessellation.saturating_sub(1)),
matrix,
colour: paint_of(&self.palettes, won).map(|p| p.rgba),
palettes: self.palettes.clone(),
chain,
item_override: reached.item,
face_overrides: reached.faces,
});
}
Ok(())
}
/// `m` × each of structure `fs`'s coordinate systems `local` (biased).
fn located(&self, m: &Matrix, fs: usize, local: &[u32]) -> Result<Matrix, PrcError> {
let globals = self.trees.get(fs).ok_or_else(|| malformed("file structure out of range".into()))?.2;
let mut placed = *m;
for &cs in local {
let l = style::biased(&globals.systems, cs).ok_or_else(|| malformed(format!("coordinate system {cs} does not exist")))?;
placed = placed.multiply(l);
}
Ok(placed)
}
}