mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 17:01:23 +00:00
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:
1 parent
7f30266790
commit
c571b13469
16 files changed
+2344
-5
No files matched your search
+12
-1
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<_>, _>>()?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -416,4 +416,4 @@ fn point_at(pts: &[[f64; 3]], idx: u32) -> Result<[f64; 3], PrcError> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub(crate) mod tests;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user