mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 18:52:36 +00:00
Merge pull request #5786 from KenM76/feat/prc-reader
M12: PRC reader crate (`pdfcraft-prc`) (#1297)
This commit is contained in:
33 files changed
+9193
-1
No files matched your search
+2
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with the SHA-256 of every asset, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||
|
||||
## In this repository (255)
|
||||
## In this repository (256)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -203,6 +203,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `vendor/hayro-interpret/assets/FoxitSymbol.pfb` | FoxitSymbol (standard-14 substitute font) | Foxit Software Inc. / PDFium Authors | BSD-3-Clause | https://pdfium.googlesource.com/pdfium (via hayro-interpret 0.7.0 assets/) | Renders non-embedded standard-14 fonts (vendored hayro-interpret) |
|
||||
| `vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc` | CGATS001Compat v2 micro ICC profile | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | CC0-1.0 | https://github.com/saucecontrol/Compact-ICC-Profiles | Default CMYK colour conversion (vendored hayro-interpret) |
|
||||
| `crates/fonts/src/std14_metrics.rs` | Standard-14 font advance widths and encodings (as Rust tables) | Adobe (Core14 AFM metrics, PDF specification encodings); tables generated by the PdfCraft contributors | MIT OR Apache-2.0 | ISO 32000-2 Annex D and the Adobe Core14 AFM metrics. Generated by joining the published WinAnsi, Symbol and ZapfDingbats encodings (code to glyph name) with the per-glyph advance widths, then spot-checked against the specification by hand in the tests of crates/fonts/src/std14.rs. Data only: the code that reads these tables lives in std14.rs, so that ordinary edits do not churn this file's hash. | Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree. |
|
||||
| `crates/prc/src/acof/table.rs` | PRC Double prefix-code table (as a Rust table) | Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2) | MIT | Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in crates/prc/src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header. | Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data. |
|
||||
| `vendor/hayro-cmap/assets/cmaps.brotli` | Adobe CMap resources (predefined CJK CMaps) | Adobe | BSD-3-Clause | https://github.com/adobe-type-tools/cmap-resources (via hayro-cmap 0.1.0 assets/) | Character-code mapping tables required to read CJK PDFs. Non-visual data, allowed by AGENTS.md §1.1 |
|
||||
| `docs/brand/artcraft-logo-white.png` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
| `docs/brand/artcraft-logo-white.svg` | ArtCraft brand: artcraft-logo-white | ArtCraft Team | LicenseRef-Storyteller-Trademark | ArtCraft Team brand assets (provided by the project owner, 2026-10-01; identical copies in storytold/photocraft) | ArtCraft branding next to the community links (About dialog, Help menu, home screen) and in docs. ArtCraft Team trademark, not open source; may be used only unmodified within PdfCraft; forks and modified versions must remove it. |
|
||||
|
||||
@@ -2156,6 +2156,17 @@ adobe_data = true
|
||||
usage = "Advance widths for the fourteen standard fonts, which a PDF may name without embedding. Non-visual data, not a typeface: the item AGENTS.md 1.1 permits, held first-party so generated appearance streams and the renderer agree."
|
||||
sha256 = "f225e0b044875b8fa6375b6b5ad935f0d99fcc1ad03ecf9f0baae2cedbfca4d2"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/prc/src/acof/table.rs"
|
||||
title = "PRC Double prefix-code table (as a Rust table)"
|
||||
author = "Kristof Ralovich (prc-rs); the code table itself is defined by the PRC format (ISO 14739-1, working draft clause 11.17.2)"
|
||||
source = "Rows taken from prc-rs src/double.rs at commit 287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d (https://github.com/ralovich/prc-rs), mechanically reformatted into (code length, code, entry) tuples; checked to be a complete prefix code by the tests in crates/prc/src/acof.rs. Data only: the code that reads the table lives in acof.rs. The MIT notice is kept in the file header."
|
||||
licence = "MIT"
|
||||
licence_file = "licenses/prc-rs-LICENSE.txt"
|
||||
kind = "data"
|
||||
usage = "Decodes every PRC Double (coordinates, transforms, colours) in 3D annotations' PRC streams. Non-visual data."
|
||||
sha256 = "8e705c5cde6a3ef778e8aca69759e38845b228b3f7f12949e587b19855d2114c"
|
||||
|
||||
[[bundled]]
|
||||
crate = "epaint_default_fonts"
|
||||
version = "0.37.0"
|
||||
|
||||
Generated
+9
@@ -4551,6 +4551,15 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-prc"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"proptest",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdfcraft-preflight"
|
||||
version = "0.18.1"
|
||||
|
||||
@@ -88,6 +88,13 @@ destination = "licenses/egui-emoji-icon-font-mit-license.txt"
|
||||
sha256 = "b9d2c1d909aa149996fd4c91dcb92b2362a04431640c1d200959da94caf8cde1"
|
||||
origin = "https://raw.githubusercontent.com/emilk/egui/0.36.2/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt"
|
||||
|
||||
[[document]]
|
||||
source = "licenses/prc-rs-LICENSE.txt"
|
||||
licences = ["MIT"]
|
||||
destination = "licenses/prc-rs-LICENSE.txt"
|
||||
sha256 = "ddb846513d92153d8fa16b18ed3e7c86953a582f0d2f49f02b29f1bd569dac58"
|
||||
origin = "https://raw.githubusercontent.com/ralovich/prc-rs/287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d/LICENSE"
|
||||
|
||||
[[document]]
|
||||
source = "vendor/hayro-interpret/LICENSE-APACHE"
|
||||
licences = ["Apache-2.0"]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "pdfcraft-prc"
|
||||
description = "PRC 3D model reader (ISO 14739-1): container, bit-level primitives, schema"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
flate2.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
@@ -0,0 +1,43 @@
|
||||
# pdfcraft-prc
|
||||
|
||||
A reader for PRC (ISO 14739-1), the 3D model format a PDF `/3D` stream of `/Subtype /PRC` carries (ISO 32000-2 §13.6.3).
|
||||
|
||||
- **Layer:** L0, standalone. No dependency on any other PdfCraft crate.
|
||||
- **Licence:** MIT OR Apache-2.0. Inflation through `flate2` with the pure-Rust `miniz_oxide` backend.
|
||||
- `#![forbid(unsafe_code)]`. Never panics on input: inflated bytes, decoded elements, schema steps and walk depth are capped, and damage returns `PrcError`.
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
PrcFile::parse(data) -> Result<PrcFile, PrcError> // header, file structures, model file
|
||||
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
|
||||
```
|
||||
|
||||
- `MAX_INFLATED_BYTES` (512 MiB) bounds every section together; `TooLarge` reports the limit hit.
|
||||
- `MAX_DECODED_ELEMENTS` (2^24) bounds the array elements one pass decodes (a tree walk, or one `tessellations` call), since a single bit can decode to a 4-8 byte element; `TooManyElements` reports it.
|
||||
- Schema programs (how a newer producer describes fields it added) run under a nesting ceiling (64) and `MAX_SCHEMA_STEPS` (2^22), shared like `MAX_DECODED_ELEMENTS` by every program one pass runs.
|
||||
- `Unsupported` names constructs the reader cannot step over (schema pointer fields, `Value_CurveIs3D`).
|
||||
- `Tessellation::Mesh` is a `TriangleMesh`: positions, triangles, one triangle range per face, the stored normals and
|
||||
texture coordinates with each triangle's corner indices into them, and per-triangle line-attribute styles.
|
||||
`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`.
|
||||
- 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`.
|
||||
- A tree's markups (PMI), views, scene lights and clipping planes are read past, not modelled; so are textures.
|
||||
- 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.
|
||||
@@ -0,0 +1,93 @@
|
||||
//! The prefix-code table behind the PRC `Double` [WD 11.17.2].
|
||||
//!
|
||||
//! The table has 2077 rows: 29 frequent doubles and one row per IEEE binary64 exponent
|
||||
//! (0..=2047). Each row is a code of 1 to 22 bits and the value it decodes to.
|
||||
//! The rows come from prc-rs under its MIT licence (see [`table`]).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
mod table;
|
||||
|
||||
/// Rows a complete table has.
|
||||
const ROWS: usize = 2077;
|
||||
|
||||
/// The longest code, in bits.
|
||||
pub(crate) const MAX_CODE_BITS: u32 = 22;
|
||||
|
||||
/// What a code decodes to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Entry {
|
||||
/// A frequent value, returned with the sign bit applied.
|
||||
Double(f64),
|
||||
/// The upper 32 bits of a binary64 (exponent set, mantissa zero); the mantissa follows
|
||||
/// in the stream.
|
||||
Exponent(u32),
|
||||
}
|
||||
|
||||
/// One row: code length, code, and what it decodes to.
|
||||
type Row = (u32, u32, Entry);
|
||||
|
||||
fn key(nbits: u32, code: u32) -> u32 {
|
||||
(nbits << 24) | code
|
||||
}
|
||||
|
||||
/// The rows keyed by `(nbits, code)`, or `None` when the table is incomplete or not prefix
|
||||
/// free (then no `Double` decodes, rather than some decoding wrongly).
|
||||
fn build(rows: &[Row]) -> Option<HashMap<u32, Entry>> {
|
||||
let mut map = HashMap::with_capacity(rows.len());
|
||||
for &(nbits, code, entry) in rows {
|
||||
if nbits == 0 || nbits > MAX_CODE_BITS || code >> nbits != 0 || map.insert(key(nbits, code), entry).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let prefix_free = map.keys().all(|&k| {
|
||||
let (n, c) = (k >> 24, k & 0xff_ffff);
|
||||
(1..n).all(|m| !map.contains_key(&key(m, c >> (n - m))))
|
||||
});
|
||||
(map.len() == ROWS && prefix_free).then_some(map)
|
||||
}
|
||||
|
||||
fn table() -> Option<&'static HashMap<u32, Entry>> {
|
||||
static MAP: OnceLock<Option<HashMap<u32, Entry>>> = OnceLock::new();
|
||||
MAP.get_or_init(|| build(table::TABLE)).as_ref()
|
||||
}
|
||||
|
||||
/// The entry for an `nbits`-long code, if one exists.
|
||||
pub(crate) fn lookup(nbits: u32, code: u32) -> Option<Entry> {
|
||||
table()?.get(&key(nbits, code)).copied()
|
||||
}
|
||||
|
||||
/// The `(nbits, code)` of IEEE exponent `e`'s row, the test writer's half of [`lookup`].
|
||||
#[cfg(test)]
|
||||
pub(crate) fn exponent_code(e: u32) -> Option<(u32, u32)> {
|
||||
table()?.iter().find(|(_, v)| matches!(v, Entry::Exponent(hi) if (hi >> 20) & 0x7ff == e)).map(|(&k, _)| (k >> 24, k & 0xff_ffff))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn an_incomplete_or_ambiguous_table_decodes_nothing() {
|
||||
assert!(build(&[(2, 0b01, Entry::Double(0.0))]).is_none(), "one row is not the table");
|
||||
let rows: Vec<Row> = (0..ROWS as u32).map(|i| (22, i, Entry::Exponent(i))).collect();
|
||||
assert!(build(&rows).is_some(), "2077 distinct 22-bit codes are prefix free");
|
||||
let mut ambiguous = rows.clone();
|
||||
ambiguous[0] = (1, 0, Entry::Double(1.0)); // a prefix of every code starting 0
|
||||
assert!(build(&ambiguous).is_none());
|
||||
let mut too_long = rows;
|
||||
too_long[0] = (2, 0b111, Entry::Double(1.0)); // a code wider than its length
|
||||
assert!(build(&too_long).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_table_is_complete() {
|
||||
let t = table().unwrap();
|
||||
assert_eq!(t.len(), ROWS);
|
||||
assert_eq!(t.values().filter(|e| matches!(e, Entry::Double(_))).count(), 29);
|
||||
assert!((0..2048).all(|e| exponent_code(e).is_some()));
|
||||
assert_eq!(lookup(2, 0b01), Some(Entry::Double(0.0)));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,420 @@
|
||||
//! The array primitives of compressed tessellation [WD 11.6-11.9] and the Huffman payload
|
||||
//! they may carry [WD 12.2].
|
||||
//!
|
||||
//! The WD gives only the Huffman writer. The reader decodes what producers write: each
|
||||
//! leaf's code length is read in the width the payload's 8-bit field states (the WD writes a
|
||||
//! fixed 8 bits), stored codes carry a leading root bit, and the payload is read LSB-first
|
||||
//! although the stream around it is MSB-first.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::PrcError;
|
||||
use crate::bits::BitReader;
|
||||
|
||||
/// Longest Huffman code [WD 12.2].
|
||||
const MAX_CODE_BITS: u32 = 32;
|
||||
|
||||
/// Widest Huffman symbol a caller asks for: the arrays hold 2- to 16-bit values.
|
||||
const MAX_SYMBOL_BITS: u32 = 16;
|
||||
|
||||
fn malformed(what: &str) -> PrcError {
|
||||
PrcError::Malformed(what.to_owned())
|
||||
}
|
||||
|
||||
/// An LSB-first cursor over a Huffman payload.
|
||||
struct Lsb<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl Lsb<'_> {
|
||||
fn bit(&mut self) -> Result<u32, PrcError> {
|
||||
if self.pos >= self.end {
|
||||
return Err(PrcError::Truncated("a Huffman payload"));
|
||||
}
|
||||
let byte = self.data.get(self.pos / 8).ok_or(PrcError::Truncated("a Huffman payload"))?;
|
||||
let b = u32::from((byte >> (self.pos % 8)) & 1);
|
||||
self.pos += 1;
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
/// `n` ≤ 32 bits, the first read being the least significant.
|
||||
fn bits(&mut self, n: u32) -> Result<u32, PrcError> {
|
||||
let mut v = 0u32;
|
||||
for i in 0..n.min(32) {
|
||||
v |= self.bit()? << i;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn remaining(&self) -> usize {
|
||||
self.end.saturating_sub(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// The low `n` (1..=16) bits of `v` as a symbol, two's complement when `signed`.
|
||||
fn symbol(v: u32, n: u32, signed: bool) -> i32 {
|
||||
let v = i64::from(v);
|
||||
let wide = if signed && n > 0 && v & (1 << (n - 1)) != 0 { v - (1i64 << n) } else { v };
|
||||
// n <= 16, so `wide` is within ±2^16.
|
||||
i32::try_from(wide).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The Huffman container of a compressed Character/Short array [WD 11.6, 11.7, 12.2]:
|
||||
/// symbols are `nbits` wide, sign-extended when `signed`.
|
||||
fn huffman(r: &mut BitReader<'_>, nbits: u32, signed: bool) -> Result<Vec<i32>, PrcError> {
|
||||
if nbits == 0 || nbits > MAX_SYMBOL_BITS {
|
||||
return Err(malformed("Huffman symbol width"));
|
||||
}
|
||||
let words = r.unsigned_integer()? as usize;
|
||||
if words == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if words.saturating_mul(32) > r.remaining() {
|
||||
return Err(PrcError::Truncated("a Huffman payload"));
|
||||
}
|
||||
// words * 32 bits remain, so words * 4 bytes cannot overflow.
|
||||
let blob = (0..words * 4).map(|_| r.character()).collect::<Result<Vec<u8>, _>>()?;
|
||||
let last = r.unsigned_integer()?;
|
||||
if last > 32 {
|
||||
return Err(malformed("Huffman last-word bit count above 32"));
|
||||
}
|
||||
let end = (words - 1) * 32 + last as usize;
|
||||
let mut b = Lsb { data: &blob, pos: 0, end };
|
||||
let codes = huffman_leaves(&mut b, nbits, signed)?;
|
||||
if codes.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let count = b.bits(32)? as usize;
|
||||
// Every element is at least one bit.
|
||||
if count > b.remaining() {
|
||||
return Err(PrcError::Truncated("a Huffman element count"));
|
||||
}
|
||||
r.take_elements(count)?;
|
||||
let mut out = Vec::with_capacity(count.min(1024));
|
||||
for _ in 0..count {
|
||||
out.push(huffman_symbol(&mut b, &codes)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The leaf table: `(code length, stored code)` to symbol.
|
||||
fn huffman_leaves(b: &mut Lsb<'_>, nbits: u32, signed: bool) -> Result<HashMap<(u32, u32), i32>, PrcError> {
|
||||
let leaves = b.bits(nbits + 1)?;
|
||||
if leaves == 0 {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
// prc-rs: the code-length width is read from the payload, not fixed at 8 bits.
|
||||
let len_width = b.bits(8)?;
|
||||
if len_width == 0 || len_width > MAX_CODE_BITS {
|
||||
return Err(malformed("Huffman code-length width"));
|
||||
}
|
||||
// Every leaf is at least nbits + 2 bits, so the payload bounds the table.
|
||||
if (leaves as usize).saturating_mul((nbits + 2) as usize) > b.remaining() {
|
||||
return Err(PrcError::Truncated("a Huffman payload"));
|
||||
}
|
||||
let mut codes = HashMap::new();
|
||||
for _ in 0..leaves {
|
||||
let s = symbol(b.bits(nbits)?, nbits, signed);
|
||||
let len = b.bits(len_width)?;
|
||||
if len == 0 || len > MAX_CODE_BITS {
|
||||
return Err(malformed("Huffman code length"));
|
||||
}
|
||||
let code = b.bits(len)?;
|
||||
if codes.insert((len, code), s).is_some() {
|
||||
return Err(malformed("duplicate Huffman code"));
|
||||
}
|
||||
}
|
||||
// A leaf whose code begins another's would end that code early, leaving its unread bits
|
||||
// in front of the next element.
|
||||
let shadowed = codes.keys().any(|&(len, code)| (1..len).any(|l| codes.contains_key(&(l, code >> (len - l)))));
|
||||
if shadowed {
|
||||
return Err(malformed("a Huffman code that is a prefix of another"));
|
||||
}
|
||||
Ok(codes)
|
||||
}
|
||||
|
||||
/// One element: code bits, most significant first, until a leaf matches.
|
||||
fn huffman_symbol(b: &mut Lsb<'_>, codes: &HashMap<(u32, u32), i32>) -> Result<i32, PrcError> {
|
||||
// prc-rs: the stored code's first bit read is its most significant (the root bit).
|
||||
let (mut code, mut len) = (0u32, 0u32);
|
||||
while len < MAX_CODE_BITS {
|
||||
code = (code << 1) | b.bit()?;
|
||||
len += 1;
|
||||
if let Some(&s) = codes.get(&(len, code)) {
|
||||
return Ok(s);
|
||||
}
|
||||
}
|
||||
Err(malformed("a Huffman code matching no leaf"))
|
||||
}
|
||||
|
||||
/// A `CharacterArray(nbits)` [WD 11.6]. `compressed` is `None` when the flag is written,
|
||||
/// else the caller's implicit value. Uncompressed elements are whole bytes, sign-extended
|
||||
/// from 8 bits when `signed`.
|
||||
pub(crate) fn character_array(r: &mut BitReader<'_>, nbits: u32, compressed: Option<bool>, signed: bool) -> Result<Vec<i32>, PrcError> {
|
||||
let compressed = match compressed {
|
||||
Some(c) => c,
|
||||
None => r.bit()?,
|
||||
};
|
||||
if compressed {
|
||||
return huffman(r, nbits, signed);
|
||||
}
|
||||
let n = r.unsigned_integer()? as usize;
|
||||
if n.saturating_mul(8) > r.remaining() {
|
||||
return Err(PrcError::Truncated("a character array"));
|
||||
}
|
||||
r.take_elements(n)?;
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
let c = r.character()?;
|
||||
Ok(if signed { i32::from(c.cast_signed()) } else { i32::from(c) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A `ShortArray(nbits)` [WD 11.7]: uncompressed elements are a low then a high byte, read
|
||||
/// unsigned.
|
||||
pub(crate) fn short_array(r: &mut BitReader<'_>, nbits: u32) -> Result<Vec<i32>, PrcError> {
|
||||
if r.bit()? {
|
||||
return huffman(r, nbits, false);
|
||||
}
|
||||
let n = r.unsigned_integer()? as usize;
|
||||
if n.saturating_mul(16) > r.remaining() {
|
||||
return Err(PrcError::Truncated("a short array"));
|
||||
}
|
||||
r.take_elements(n)?;
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
let lo = r.character()?;
|
||||
let hi = r.character()?;
|
||||
Ok(i32::from(u16::from_le_bytes([lo, hi])))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A `CompressedIntegerArray` [WD 11.8]: a CharacterArray(6) of widths, then one
|
||||
/// `IntegerWithVariableBitNumber` per width.
|
||||
pub(crate) fn compressed_integer_array(r: &mut BitReader<'_>) -> Result<Vec<i64>, PrcError> {
|
||||
let widths = character_array(r, 6, None, false)?;
|
||||
widths
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
let w = u32::try_from(w).map_err(|_| malformed("integer width"))?;
|
||||
r.integer_vbn(w)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A `CompressedIndiceArray` [WD 11.9], read as producers write it: the stored signed 6-bit
|
||||
/// values are width differences summed into each element's width, and each element is a
|
||||
/// delta from the previous (the WD's own reading desyncs from the third element). Indices
|
||||
/// are never negative.
|
||||
pub(crate) fn compressed_indice_array(r: &mut BitReader<'_>, compressed: Option<bool>) -> Result<Vec<u32>, PrcError> {
|
||||
let diffs = character_array(r, 6, compressed, true)?;
|
||||
let mut out = Vec::with_capacity(diffs.len());
|
||||
let (mut width, mut prev) = (0i64, 0i64);
|
||||
for d in diffs {
|
||||
// prc-rs: widths are running sums of the stored differences, values running deltas.
|
||||
width = width.checked_add(i64::from(d)).ok_or_else(|| malformed("indice width"))?;
|
||||
let w = u32::try_from(width).map_err(|_| malformed("indice width"))?;
|
||||
let v = prev.checked_add(r.integer_vbn(w)?).ok_or_else(|| malformed("indice delta"))?;
|
||||
out.push(u32::try_from(v).map_err(|_| malformed("an index outside 0..2^32"))?);
|
||||
prev = v;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// An `UncompressedBoolArray` of `n` raw bits (no length prefix).
|
||||
pub(crate) fn bool_array(r: &mut BitReader<'_>, n: usize) -> Result<Vec<bool>, PrcError> {
|
||||
if n > r.remaining() {
|
||||
return Err(PrcError::Truncated("a boolean array"));
|
||||
}
|
||||
r.take_elements(n)?;
|
||||
(0..n).map(|_| r.bit()).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use super::*;
|
||||
use crate::testw::W;
|
||||
|
||||
fn read<T>(w: &W, f: impl FnOnce(&mut BitReader<'_>) -> Result<T, PrcError>) -> T {
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
let v = f(&mut r).unwrap();
|
||||
assert_eq!(r.position(), w.len(), "bits consumed");
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huffman_decodes_the_wd_example_with_root_bits() {
|
||||
// WD 12.2's input {1,3,5,7,7,9,5,3,3}; codes carry the root bit.
|
||||
let leaves = [(3, 0b100, 3), (7, 0b101, 3), (5, 0b110, 3), (1, 0b1110, 4), (9, 0b1111, 4)];
|
||||
let values = [1, 3, 5, 7, 7, 9, 5, 3, 3];
|
||||
let mut w = W::default();
|
||||
w.bit(true).huffman(4, 3, &leaves, &values);
|
||||
assert_eq!(read(&w, |r| character_array(r, 4, None, false)), values);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huffman_symbols_sign_extend_only_when_signed() {
|
||||
let leaves = [(0b111110, 0b10, 2), (1, 0b11, 2)];
|
||||
let mut w = W::default();
|
||||
w.huffman(6, 2, &leaves, &[62, 1, 62]);
|
||||
assert_eq!(read(&w, |r| huffman(r, 6, true)), [-2, 1, -2]);
|
||||
assert_eq!(read(&w, |r| huffman(r, 6, false)), [62, 1, 62]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huffman_rejects_codes_that_match_no_leaf_and_empty_payloads_are_empty() {
|
||||
let mut bad = W::default();
|
||||
bad.huffman_raw(2, 2, &[(1, 0b10, 2)], &[(0b11, 2)]);
|
||||
let bytes = bad.bytes();
|
||||
assert!(matches!(huffman(&mut BitReader::new(&bytes), 2, false), Err(PrcError::Malformed(_) | PrcError::Truncated(_))));
|
||||
let mut empty = W::default();
|
||||
empty.uint(0);
|
||||
assert!(read(&empty, |r| huffman(r, 2, false)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huffman_element_count_past_the_payload_is_truncated_not_allocated() {
|
||||
// One leaf, then an element count of 2^32 - 1 with no elements behind it.
|
||||
let mut w = W::default();
|
||||
w.huffman_count(2, 2, &[(1, 0b10, 2)], u32::MAX);
|
||||
let bytes = w.bytes();
|
||||
assert!(matches!(huffman(&mut BitReader::new(&bytes), 2, false), Err(PrcError::Truncated("a Huffman element count"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huffman_code_that_begins_another_is_malformed() {
|
||||
// `10` would end every `100` one bit early.
|
||||
let mut w = W::default();
|
||||
w.huffman(2, 2, &[(1, 0b10, 2), (2, 0b100, 3)], &[2, 1]);
|
||||
let bytes = w.bytes();
|
||||
let r = huffman(&mut BitReader::new(&bytes), 2, false);
|
||||
assert!(malformed_by(r, "a Huffman code that is a prefix of another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huffman_widths_out_of_range_are_errors() {
|
||||
// Well-formed payloads at each width, which only the width check refuses.
|
||||
for nbits in [0, 17] {
|
||||
let mut w = W::default();
|
||||
w.huffman(nbits, 2, &[(1, 0b10, 2)], &[1]);
|
||||
let bytes = w.bytes();
|
||||
assert!(matches!(huffman(&mut BitReader::new(&bytes), nbits, false), Err(PrcError::Malformed(_))), "{nbits}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uncompressed_arrays() {
|
||||
let mut w = W::default();
|
||||
w.bit(false).uint(2).put(0xfe, 8).put(3, 8);
|
||||
assert_eq!(read(&w, |r| character_array(r, 6, None, true)), [-2, 3]);
|
||||
let mut s = W::default();
|
||||
s.bit(false).uint(1).put(0x34, 8).put(0x12, 8);
|
||||
assert_eq!(read(&s, |r| short_array(r, 16)), [0x1234]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_array_reads_one_width_per_value() {
|
||||
let mut w = W::default();
|
||||
w.bit(false).uint(3).put(3, 8).put(0, 8).put(4, 8);
|
||||
w.bit(true).put(3, 2); // -3 in 3 bits
|
||||
w.bit(false).put(5, 3); // 5 in 4 bits
|
||||
assert_eq!(read(&w, compressed_integer_array), [-3, 0, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indice_array_sums_widths_and_deltas() {
|
||||
// Values 5, 6, 4: widths 4, 2, 3 stored as diffs 4, -2, +1.
|
||||
let mut w = W::default();
|
||||
w.bit(false).uint(3).put(4, 8).put(0xfe, 8).put(1, 8);
|
||||
w.bit(false).put(5, 3); // 5
|
||||
w.bit(false).put(1, 1); // +1
|
||||
w.bit(true).put(2, 2); // -2
|
||||
assert_eq!(read(&w, |r| compressed_indice_array(r, None)), [5, 6, 4]);
|
||||
// The implicit-flag form reads no flag bit.
|
||||
let mut i = W::default();
|
||||
i.uint(1).put(2, 8).bit(false).put(1, 1);
|
||||
assert_eq!(read(&i, |r| compressed_indice_array(r, Some(false))), [1]);
|
||||
// A negative running index is damage.
|
||||
let mut n = W::default();
|
||||
n.bit(false).uint(1).put(2, 8).bit(true).put(1, 1);
|
||||
let bytes = n.bytes();
|
||||
assert!(compressed_indice_array(&mut BitReader::new(&bytes), None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bool_array_longer_than_the_data_is_truncated() {
|
||||
assert!(matches!(bool_array(&mut BitReader::new(&[0]), 9), Err(PrcError::Truncated(_))));
|
||||
assert_eq!(bool_array(&mut BitReader::new(&[0x80]), 2).unwrap(), [true, false]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_elements_are_charged_against_one_allowance() {
|
||||
// One bit can decode to a 4-byte element, so the byte cap alone does not bound memory.
|
||||
let leaves = [(0b111110, 0b10, 2), (1, 0b11, 2)];
|
||||
let mut w = W::default();
|
||||
w.huffman(6, 2, &leaves, &[62, 1, 62]);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::with_elements(&bytes, 2);
|
||||
assert!(matches!(huffman(&mut r, 6, false), Err(PrcError::TooManyElements { .. })));
|
||||
let mut r = BitReader::with_elements(&bytes, 4);
|
||||
assert_eq!(huffman(&mut r, 6, false).unwrap(), [62, 1, 62]);
|
||||
assert_eq!(r.elements_left(), 1);
|
||||
assert!(matches!(bool_array(&mut BitReader::with_elements(&[0], 1), 2), Err(PrcError::TooManyElements { .. })));
|
||||
}
|
||||
|
||||
/// Uncompressed character and short arrays are charged against the allowance too, and a
|
||||
/// character count past the data is refused by name before anything is read.
|
||||
#[test]
|
||||
fn uncompressed_arrays_are_charged_and_their_counts_checked() {
|
||||
let mut w = W::default();
|
||||
w.uint(3).put(0, 8).put(1, 8).put(2, 8);
|
||||
let chars = w.bytes();
|
||||
let read = |n| character_array(&mut BitReader::with_elements(&chars, n), 8, Some(false), false);
|
||||
assert!(matches!(read(2), Err(PrcError::TooManyElements { .. })));
|
||||
assert_eq!(read(3).unwrap(), [0, 1, 2]);
|
||||
let mut w = W::default();
|
||||
w.bit(false).uint(2).put(1, 8).put(0, 8).put(2, 8).put(0, 8);
|
||||
let shorts = w.bytes();
|
||||
assert!(matches!(short_array(&mut BitReader::with_elements(&shorts, 1), 8), Err(PrcError::TooManyElements { .. })));
|
||||
assert_eq!(short_array(&mut BitReader::with_elements(&shorts, 2), 8).unwrap(), [1, 2]);
|
||||
let mut w = W::default();
|
||||
w.uint(1000).put(7, 8);
|
||||
let short_of_data = w.bytes();
|
||||
let res = character_array(&mut BitReader::new(&short_of_data), 8, Some(false), false);
|
||||
assert!(matches!(res, Err(PrcError::Truncated("a character array"))), "{res:?}");
|
||||
}
|
||||
|
||||
fn malformed_by(res: Result<Vec<i32>, PrcError>, what: &str) -> bool {
|
||||
matches!(res, Err(PrcError::Malformed(m)) if m == what)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_code_length_width_or_length_is_refused_by_name() {
|
||||
let mut w = W::default();
|
||||
w.huffman_raw(2, 0, &[(1, 0, 0)], &[]);
|
||||
let bytes = w.bytes();
|
||||
assert!(malformed_by(huffman(&mut BitReader::new(&bytes), 2, false), "Huffman code-length width"));
|
||||
let mut w = W::default();
|
||||
w.huffman_raw(2, 2, &[(1, 0, 0)], &[]);
|
||||
let bytes = w.bytes();
|
||||
assert!(malformed_by(huffman(&mut BitReader::new(&bytes), 2, false), "Huffman code length"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_boolean_array_past_the_data_is_refused_before_it_is_read() {
|
||||
let mut w = W::default();
|
||||
w.put(0b1011, 4);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
let n = r.remaining() + 1;
|
||||
assert_eq!(bool_array(&mut r, n), Err(PrcError::Truncated("a boolean array")));
|
||||
assert_eq!(r.position(), 0, "nothing read");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
//! The bit-level reader for PRC compressed sections [WD 10.3, 11.1-11.17].
|
||||
//!
|
||||
//! Values are consumed MSB-first from each inflated byte and straddle byte boundaries freely
|
||||
//! [WD 10.3.1, 11.3]. Multi-byte integers are groups of eight bits, low group first. Two sign
|
||||
//! conventions coexist: `Integer` is two's complement, the `*WithVariableBitNumber` types are
|
||||
//! sign-magnitude.
|
||||
|
||||
use crate::PrcError;
|
||||
use crate::acof::{self, Entry};
|
||||
|
||||
/// The most array elements the readers of one PRC stream decode together (2^24): at most 8
|
||||
/// bytes each, so 128 MiB.
|
||||
pub const MAX_DECODED_ELEMENTS: usize = 1 << 24;
|
||||
|
||||
/// The most schema-program steps (tokens executed, loop iterations included) the readers of
|
||||
/// one PRC stream run together.
|
||||
pub const MAX_SCHEMA_STEPS: u64 = 1 << 22;
|
||||
|
||||
/// What the readers of one stream may still spend, carried from each reader to the next.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Budget {
|
||||
/// Array elements still to decode.
|
||||
pub(crate) elements: usize,
|
||||
/// Schema-program steps still to run.
|
||||
pub(crate) steps: u64,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
/// A whole stream's allowance.
|
||||
pub(crate) const FULL: Self = Self { elements: MAX_DECODED_ELEMENTS, steps: MAX_SCHEMA_STEPS };
|
||||
}
|
||||
|
||||
/// A cursor over one inflated section.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BitReader<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
budget: Budget,
|
||||
}
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
/// A reader positioned at the first bit of `data`, allowed [`MAX_DECODED_ELEMENTS`] and
|
||||
/// [`MAX_SCHEMA_STEPS`].
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self::with_budget(data, Budget::FULL)
|
||||
}
|
||||
|
||||
/// A reader of `data` allowed `budget`: what an earlier reader of the same stream left.
|
||||
pub(crate) fn with_budget(data: &'a [u8], budget: Budget) -> Self {
|
||||
Self { data, pos: 0, budget }
|
||||
}
|
||||
|
||||
/// A reader of `data` allowed `elements` array elements and a full step allowance.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_elements(data: &'a [u8], elements: usize) -> Self {
|
||||
Self::with_budget(data, Budget { elements, ..Budget::FULL })
|
||||
}
|
||||
|
||||
/// This reader, at its position, allowed `budget` instead: what readers of the same
|
||||
/// stream left since it stopped.
|
||||
pub(crate) fn with_remaining(mut self, budget: Budget) -> Self {
|
||||
self.budget = budget;
|
||||
self
|
||||
}
|
||||
|
||||
/// What this reader may still spend.
|
||||
pub(crate) fn budget(&self) -> Budget {
|
||||
self.budget
|
||||
}
|
||||
|
||||
/// Array elements this reader may still decode.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn elements_left(&self) -> usize {
|
||||
self.budget.elements
|
||||
}
|
||||
|
||||
/// Charge `n` decoded array elements against the allowance.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::TooManyElements`] when fewer than `n` are left.
|
||||
pub(crate) fn take_elements(&mut self, n: usize) -> Result<(), PrcError> {
|
||||
self.budget.elements = self.budget.elements.checked_sub(n).ok_or(PrcError::TooManyElements { limit: MAX_DECODED_ELEMENTS })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Charge one schema-program step; false when none is left.
|
||||
pub(crate) fn take_step(&mut self) -> bool {
|
||||
match self.budget.steps.checked_sub(1) {
|
||||
Some(left) => {
|
||||
self.budget.steps = left;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bits consumed so far.
|
||||
pub fn position(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
|
||||
/// Bits left before the end of the data.
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.data.len().saturating_mul(8).saturating_sub(self.pos)
|
||||
}
|
||||
|
||||
/// Step over `n` bits of opaque data (`UserData` [WD 8.6]).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`] when fewer than `n` bits remain; the cursor does not move.
|
||||
pub fn skip_bits(&mut self, n: usize) -> Result<(), PrcError> {
|
||||
if n > self.remaining() {
|
||||
return Err(PrcError::Truncated("user data"));
|
||||
}
|
||||
self.pos = self.pos.saturating_add(n);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One bit; a PRC `Boolean` [WD 10.3.3].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`] at the end of the data.
|
||||
pub fn bit(&mut self) -> Result<bool, PrcError> {
|
||||
let byte = self.data.get(self.pos / 8).ok_or(PrcError::Truncated("a bit"))?;
|
||||
let b = (byte >> (7 - (self.pos % 8))) & 1 == 1;
|
||||
// `pos / 8` indexed the data, so `pos + 1` is at most `8 * len`.
|
||||
self.pos = self.pos.saturating_add(1);
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
/// `n` bits (at most 32), MSB-first; `UnsignedIntegerWithVariableBitNumber` [WD 11.13].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`] at the end of the data; [`PrcError::Malformed`] for `n > 32`.
|
||||
pub fn bits(&mut self, n: u32) -> Result<u32, PrcError> {
|
||||
if n > 32 {
|
||||
return Err(PrcError::Malformed(format!("{n}-bit field")));
|
||||
}
|
||||
if self.remaining() < n as usize {
|
||||
return Err(PrcError::Truncated("a bit field"));
|
||||
}
|
||||
let mut v: u64 = 0;
|
||||
for _ in 0..n {
|
||||
v = (v << 1) | u64::from(self.bit()?);
|
||||
}
|
||||
// n <= 32, so v fits.
|
||||
Ok(u32::try_from(v).unwrap_or(u32::MAX))
|
||||
}
|
||||
|
||||
fn byte(&mut self) -> Result<u8, PrcError> {
|
||||
Ok(u8::try_from(self.bits(8)?).unwrap_or(u8::MAX))
|
||||
}
|
||||
|
||||
/// A `Character`: one byte [WD 10.3.4].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`].
|
||||
pub fn character(&mut self) -> Result<u8, PrcError> {
|
||||
self.byte()
|
||||
}
|
||||
|
||||
/// An `UnsignedInteger` [WD 11.10]: while a 1 bit precedes it, one more eight-bit group,
|
||||
/// low group first.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`]; [`PrcError::Malformed`] past four groups.
|
||||
pub fn unsigned_integer(&mut self) -> Result<u32, PrcError> {
|
||||
let mut v: u32 = 0;
|
||||
let mut shift = 0;
|
||||
while self.bit()? {
|
||||
if shift >= 32 {
|
||||
return Err(PrcError::Malformed("UnsignedInteger over 32 bits".into()));
|
||||
}
|
||||
v |= u32::from(self.byte()?) << shift;
|
||||
shift += 8;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// An `Integer` [WD 11.11]: a 0 bit is zero; otherwise eight-bit groups, low group first,
|
||||
/// each followed by a continue bit, sign-extended from the last group's top bit.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`]; [`PrcError::Malformed`] past four groups.
|
||||
pub fn integer(&mut self) -> Result<i32, PrcError> {
|
||||
if !self.bit()? {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut v: u32 = 0;
|
||||
let mut shift = 0;
|
||||
loop {
|
||||
if shift >= 32 {
|
||||
return Err(PrcError::Malformed("Integer over 32 bits".into()));
|
||||
}
|
||||
let group = self.byte()?;
|
||||
v |= u32::from(group) << shift;
|
||||
shift += 8;
|
||||
if !self.bit()? {
|
||||
if group & 0x80 != 0 && shift < 32 {
|
||||
v |= u32::MAX << shift;
|
||||
}
|
||||
return Ok(v.cast_signed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An `IntegerWithVariableBitNumber(n)` [WD 11.12]: a negative flag (1 = negative; the WD
|
||||
/// prose says the opposite, its sample code and real files agree on this), then the
|
||||
/// magnitude in `n - 1` bits.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`]; [`PrcError::Malformed`] for `n > 33`.
|
||||
pub fn integer_vbn(&mut self, n: u32) -> Result<i64, PrcError> {
|
||||
let Some(magnitude_bits) = n.checked_sub(1) else {
|
||||
return Ok(0);
|
||||
};
|
||||
let negative = self.bit()?;
|
||||
let m = i64::from(self.bits(magnitude_bits)?);
|
||||
Ok(if negative { -m } else { m })
|
||||
}
|
||||
|
||||
/// A `DoubleWithVariableBitNumber(tolerance, n)` [WD 11.14]: a sign-magnitude quantum
|
||||
/// count times `tolerance`.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Self::integer_vbn`].
|
||||
pub fn double_vbn(&mut self, tolerance: f64, n: u32) -> Result<f64, PrcError> {
|
||||
Ok(self.integer_vbn(n)? as f64 * tolerance)
|
||||
}
|
||||
|
||||
/// `NumberOfBitsThenUnsignedInteger` [WD 11.15]: a 5-bit width, then the value in that
|
||||
/// many bits.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`].
|
||||
pub fn nbits_then_unsigned(&mut self) -> Result<u32, PrcError> {
|
||||
let n = self.bits(5)?;
|
||||
self.bits(n)
|
||||
}
|
||||
|
||||
/// A `String` [WD 10.3.7, 11.4]: `None` for the null string, else UTF-8 (invalid
|
||||
/// sequences replaced).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`] when the byte count runs past the data.
|
||||
pub fn string(&mut self) -> Result<Option<String>, PrcError> {
|
||||
if !self.bit()? {
|
||||
return Ok(None);
|
||||
}
|
||||
let len = self.unsigned_integer()? as usize;
|
||||
if len.saturating_mul(8) > self.remaining() {
|
||||
return Err(PrcError::Truncated("a String"));
|
||||
}
|
||||
let bytes = (0..len).map(|_| self.byte()).collect::<Result<Vec<u8>, _>>()?;
|
||||
Ok(Some(String::from_utf8_lossy(&bytes).into_owned()))
|
||||
}
|
||||
|
||||
/// `FloatAsBytes` [WD 11.5]: an IEEE binary32, least-significant byte first.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`].
|
||||
pub fn float_as_bytes(&mut self) -> Result<f32, PrcError> {
|
||||
let b = [self.byte()?, self.byte()?, self.byte()?, self.byte()?];
|
||||
Ok(f32::from_le_bytes(b))
|
||||
}
|
||||
|
||||
/// A `Double` [WD 11.17]: a prefix code from the Double table, then a sign bit and, for
|
||||
/// an exponent code, a byte-wise mantissa with back-references.
|
||||
///
|
||||
/// The two-bit zero code has no sign bit. Back-reference offsets 1 to 5 are all accepted,
|
||||
/// distance 1 included (PDF Association pdf-issues #769).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`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 {
|
||||
code = (code << 1) | u32::from(self.bit()?);
|
||||
if let Some(e) = acof::lookup(n, code) {
|
||||
if n == 2 && code == 0b01 {
|
||||
return Ok(0.0);
|
||||
}
|
||||
entry = Some(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let entry = entry.ok_or(PrcError::UnknownDoubleCode)?;
|
||||
let negative = self.bit()?;
|
||||
let upper = match entry {
|
||||
Entry::Double(v) => return Ok(if negative { -v } else { v }),
|
||||
Entry::Exponent(upper) => upper,
|
||||
};
|
||||
let mut b = (u64::from(upper) << 32).to_le_bytes();
|
||||
if self.bit()? {
|
||||
self.mantissa(&mut b)?;
|
||||
}
|
||||
let sign = if negative { 1u64 << 63 } else { 0 };
|
||||
Ok(f64::from_bits(u64::from_le_bytes(b) | sign))
|
||||
}
|
||||
|
||||
/// Mantissa bits 51..0 into `b` (little-endian byte order): a nibble, then bytes 5 down
|
||||
/// to 0, each a literal or a back-reference to a byte already set.
|
||||
fn mantissa(&mut self, b: &mut [u8; 8]) -> Result<(), PrcError> {
|
||||
let nibble = u8::try_from(self.bits(4)?).unwrap_or(0);
|
||||
let [.., b6, _] = b;
|
||||
*b6 |= nibble;
|
||||
let at = |b: &[u8; 8], i: usize| b.get(i).copied().ok_or_else(|| PrcError::Malformed("Double back-reference past the value".into()));
|
||||
for cbi in (0..=5usize).rev() {
|
||||
let prev = at(b, cbi + 1)?;
|
||||
let value = if self.bit()? {
|
||||
self.byte()?
|
||||
} else {
|
||||
match self.bits(3)? {
|
||||
// Fill this byte and every lower one with the previous byte.
|
||||
0 => {
|
||||
b.iter_mut().take(cbi + 1).for_each(|x| *x = prev);
|
||||
return Ok(());
|
||||
}
|
||||
// As 0, but the lowest byte is a literal.
|
||||
6 => {
|
||||
b.iter_mut().take(cbi + 1).skip(1).for_each(|x| *x = prev);
|
||||
let low = self.byte()?;
|
||||
let [b0, ..] = b;
|
||||
*b0 = low;
|
||||
return Ok(());
|
||||
}
|
||||
off @ 1..=5 => at(b, cbi + off as usize)?,
|
||||
_ => return Err(PrcError::Malformed("Double mantissa offset 7".into())),
|
||||
}
|
||||
};
|
||||
if let Some(x) = b.get_mut(cbi) {
|
||||
*x = value;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testw::W;
|
||||
|
||||
/// The code for IEEE exponent `e`.
|
||||
fn exponent_code(e: u32) -> (u32, u32) {
|
||||
acof::exponent_code(e).expect("the table has a row per exponent")
|
||||
}
|
||||
|
||||
fn read_double(w: &W) -> f64 {
|
||||
BitReader::new(&w.bytes()).double().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_is_the_two_bit_code_with_no_sign() {
|
||||
let mut w = W::default();
|
||||
w.put(0b01, 2).put(0b1111_1111, 8);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
assert_eq!(r.double().unwrap(), 0.0);
|
||||
assert_eq!(r.position(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exponent_with_literal_mantissa() {
|
||||
let v = -1234.5678f64;
|
||||
let le = v.to_bits().to_le_bytes();
|
||||
let e = u32::try_from((v.to_bits() >> 52) & 0x7ff).unwrap();
|
||||
let (n, c) = exponent_code(e);
|
||||
let mut w = W::default();
|
||||
w.put(u64::from(c), n).put(1, 1).put(1, 1).put(u64::from(le[6] & 0x0f), 4);
|
||||
for byte in le[..6].iter().rev() {
|
||||
w.put(1, 1).put(u64::from(*byte), 8);
|
||||
}
|
||||
assert_eq!(read_double(&w), v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exponent_without_mantissa_is_a_power_of_two() {
|
||||
let (n, c) = exponent_code(1023 + 3);
|
||||
let mut w = W::default();
|
||||
w.put(u64::from(c), n).put(0, 1).put(0, 1);
|
||||
assert_eq!(read_double(&w), 8.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_back_reference_and_literal_low_byte() {
|
||||
// Mantissa bytes 6..0 = 0x05, 0xAA, 0xAA (offset 1), 0xAA (offset 2), fill 0xAA to byte 1, byte 0 = 0x11.
|
||||
let (n, c) = exponent_code(1023);
|
||||
let mut w = W::default();
|
||||
w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0x5, 4);
|
||||
w.put(1, 1).put(0xAA, 8);
|
||||
w.put(0, 1).put(1, 3);
|
||||
w.put(0, 1).put(2, 3);
|
||||
w.put(0, 1).put(6, 3).put(0x11, 8);
|
||||
assert_eq!(read_double(&w), f64::from_le_bytes([0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xF5, 0x3F]));
|
||||
|
||||
// Byte 5 literal, then fill 4..0.
|
||||
let mut w = W::default();
|
||||
w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0x5, 4);
|
||||
w.put(1, 1).put(0x77, 8).put(0, 1).put(0, 3);
|
||||
assert_eq!(read_double(&w), f64::from_le_bytes([0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0xF5, 0x3F]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_back_reference_past_the_value_is_refused() {
|
||||
let (n, c) = exponent_code(1023);
|
||||
let mut w = W::default();
|
||||
w.put(u64::from(c), n).put(0, 1).put(1, 1).put(0, 4);
|
||||
w.put(0, 1).put(5, 3).put(0, 16); // byte 5 + offset 5 = byte 10
|
||||
assert!(matches!(BitReader::new(&w.bytes()).double(), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_truncated_double_is_an_error_not_a_panic() {
|
||||
// No code is 8 bits or shorter and a prefix of 0x17, so one byte ends inside a code.
|
||||
assert!(matches!(BitReader::new(&[0x17]).double(), Err(PrcError::Truncated(_))));
|
||||
assert!(matches!(BitReader::new(&[]).double(), Err(PrcError::Truncated(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integers_and_strings() {
|
||||
let mut w = W::default();
|
||||
// UnsignedInteger 300 = groups 0x2C, 0x01.
|
||||
w.put(1, 1).put(0x2C, 8).put(1, 1).put(0x01, 8).put(0, 1);
|
||||
// Integer -2 = group 0xFE, stop.
|
||||
w.put(1, 1).put(0xFE, 8).put(0, 1);
|
||||
// Integer 200 = 0xC8 then 0x00 (top bit clear), stop.
|
||||
w.put(1, 1).put(0xC8, 8).put(1, 1).put(0x00, 8).put(0, 1);
|
||||
// IntegerWithVariableBitNumber(4) = -5.
|
||||
w.put(1, 1).put(5, 3);
|
||||
// String "hé".
|
||||
w.put(1, 1).put(1, 1).put(3, 8).put(0, 1).put(0x68, 8).put(0xC3, 8).put(0xA9, 8);
|
||||
// Null string.
|
||||
w.put(0, 1);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
assert_eq!(r.unsigned_integer().unwrap(), 300);
|
||||
assert_eq!(r.integer().unwrap(), -2);
|
||||
assert_eq!(r.integer().unwrap(), 200);
|
||||
assert_eq!(r.integer_vbn(4).unwrap(), -5);
|
||||
assert_eq!(r.string().unwrap().as_deref(), Some("hé"));
|
||||
assert_eq!(r.string().unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_writer_round_trips_through_the_reader() {
|
||||
let mut w = W::default();
|
||||
w.uint(0).uint(u32::MAX).int(-129).int(i32::MIN).int(i32::MAX).string(Some("glass")).string(None);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
assert_eq!(r.unsigned_integer().unwrap(), 0);
|
||||
assert_eq!(r.unsigned_integer().unwrap(), u32::MAX);
|
||||
assert_eq!(r.integer().unwrap(), -129);
|
||||
assert_eq!(r.integer().unwrap(), i32::MIN);
|
||||
assert_eq!(r.integer().unwrap(), i32::MAX);
|
||||
assert_eq!(r.string().unwrap().as_deref(), Some("glass"));
|
||||
assert_eq!(r.string().unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_groups_and_widths_are_malformed() {
|
||||
// Five UnsignedInteger groups.
|
||||
let mut w = W::default();
|
||||
(0..5).for_each(|_| {
|
||||
w.put(1, 1).put(0xFF, 8);
|
||||
});
|
||||
w.put(0, 1);
|
||||
assert!(matches!(BitReader::new(&w.bytes()).unsigned_integer(), Err(PrcError::Malformed(_))));
|
||||
assert!(matches!(BitReader::new(&[0; 8]).bits(33), Err(PrcError::Malformed(_))));
|
||||
assert!(matches!(BitReader::new(&[0; 8]).integer_vbn(34), Err(PrcError::Malformed(_))));
|
||||
assert_eq!(BitReader::new(&[]).integer_vbn(0), Ok(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_is_an_error_not_a_panic() {
|
||||
let mut r = BitReader::new(&[0xFF]);
|
||||
assert!(r.unsigned_integer().is_err());
|
||||
let mut r = BitReader::new(&[0xC0, 0x40]);
|
||||
assert!(matches!(r.string(), Err(PrcError::Truncated(_))));
|
||||
let mut r = BitReader::new(&[0xAB]);
|
||||
assert!(matches!(r.skip_bits(9), Err(PrcError::Truncated(_))));
|
||||
assert_eq!(r.position(), 0, "a refused skip does not move the cursor");
|
||||
r.skip_bits(8).unwrap();
|
||||
assert_eq!(r.remaining(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
//! The PRC container: file header, file-structure descriptions, section extents and
|
||||
//! inflation [WD 6.1-6.2, 10.2].
|
||||
//!
|
||||
//! Section extents are not stored: each section runs to the next one's start offset, and the
|
||||
//! last one to the model file's start. Inflation stops at the zlib stream's end, so padding
|
||||
//! between sections is tolerated.
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use crate::PrcError;
|
||||
use crate::bits::BitReader;
|
||||
use crate::schema::Schema;
|
||||
|
||||
/// The PRC version this reader implements.
|
||||
pub const PRC_READER_VERSION: u32 = 8137;
|
||||
|
||||
/// The most file structures [`PrcFile::parse`] accepts.
|
||||
pub const MAX_FILE_STRUCTURES: usize = 65_536;
|
||||
|
||||
/// Ceiling on the total inflated size of every section in one PRC stream, together with the
|
||||
/// picture and uncompressed blocks it copies.
|
||||
pub const MAX_INFLATED_BYTES: usize = 512 * 1024 * 1024;
|
||||
|
||||
/// Sections per file structure [WD 6.1.2]: its header, then the five in [`SectionKind`]
|
||||
/// order.
|
||||
const SECTIONS: usize = 6;
|
||||
|
||||
/// A 128-bit PRC identifier [WD 10.2.4]: four little-endian `u32`s.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct UniqueId(pub [u32; 4]);
|
||||
|
||||
/// The compressed sections of one file structure, in file order [WD 6.1.2, 6.2.1].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SectionKind {
|
||||
/// Starts with the file structure's schema, then its globals entity.
|
||||
Globals,
|
||||
/// The product-structure tree.
|
||||
Tree,
|
||||
/// Tessellated (mesh) data.
|
||||
Tessellation,
|
||||
/// Exact (B-rep) geometry.
|
||||
Geometry,
|
||||
/// Supplementary geometry.
|
||||
ExtraGeometry,
|
||||
}
|
||||
|
||||
impl SectionKind {
|
||||
/// Every kind, in file order.
|
||||
pub const ALL: [SectionKind; 5] =
|
||||
[SectionKind::Globals, SectionKind::Tree, SectionKind::Tessellation, SectionKind::Geometry, SectionKind::ExtraGeometry];
|
||||
|
||||
/// The section's name, as used in errors.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
SectionKind::Globals => "globals",
|
||||
SectionKind::Tree => "tree",
|
||||
SectionKind::Tessellation => "tessellation",
|
||||
SectionKind::Geometry => "geometry",
|
||||
SectionKind::ExtraGeometry => "extra geometry",
|
||||
}
|
||||
}
|
||||
|
||||
fn index(self) -> usize {
|
||||
match self {
|
||||
SectionKind::Globals => 0,
|
||||
SectionKind::Tree => 1,
|
||||
SectionKind::Tessellation => 2,
|
||||
SectionKind::Geometry => 3,
|
||||
SectionKind::ExtraGeometry => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The uncompressed file header [WD 6.1.1].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct PrcHeader {
|
||||
/// The oldest reader version the producer says can read the file.
|
||||
pub min_version_for_read: u32,
|
||||
/// The producer's PRC version.
|
||||
pub authoring_version: u32,
|
||||
/// The file's identifier.
|
||||
pub file_id: UniqueId,
|
||||
/// The producing application's identifier (all zero = unregistered).
|
||||
pub application_id: UniqueId,
|
||||
}
|
||||
|
||||
/// One file structure: its uncompressed header fields and its five inflated sections
|
||||
/// [WD 6.2].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct FileStructure {
|
||||
/// The identifier; equals the file header's description of it.
|
||||
pub id: UniqueId,
|
||||
/// Per-structure reader version floor; may differ from the file's.
|
||||
pub min_version_for_read: u32,
|
||||
/// Per-structure producer version.
|
||||
pub authoring_version: u32,
|
||||
/// Picture payloads (JPEG, PNG, ...) carried uncompressed in the header, referenced by
|
||||
/// index from picture entities [WD 6.2.2].
|
||||
pub pictures: Vec<Vec<u8>>,
|
||||
sections: [Vec<u8>; 5],
|
||||
}
|
||||
|
||||
impl FileStructure {
|
||||
/// The inflated bytes of one section.
|
||||
pub fn section(&self, kind: SectionKind) -> &[u8] {
|
||||
self.sections.get(kind.index()).map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
/// The globals section's leading schema: the fields producers newer than this reader
|
||||
/// added to each entity type [WD 6.3].
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Schema::read`].
|
||||
pub fn schema(&self) -> Result<Schema, PrcError> {
|
||||
Schema::read(&mut BitReader::new(self.section(SectionKind::Globals)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed PRC stream: header, file structures and model-file section.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct PrcFile {
|
||||
/// The file header.
|
||||
pub header: PrcHeader,
|
||||
/// File structures in physical order (referenced before referencing).
|
||||
pub file_structures: Vec<FileStructure>,
|
||||
/// The inflated model-file section: its schema, then the model file entity.
|
||||
pub model_file: Vec<u8>,
|
||||
/// The `UncompressedFiles` blocks after the file header [WD 6.1.1], which picture
|
||||
/// entities may also reference.
|
||||
pub uncompressed_files: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// A little-endian byte cursor over the uncompressed parts of the stream.
|
||||
struct Bytes<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Bytes<'a> {
|
||||
fn take(&mut self, n: usize, what: &'static str) -> Result<&'a [u8], PrcError> {
|
||||
let end = self.pos.checked_add(n).ok_or(PrcError::Truncated(what))?;
|
||||
let s = self.data.get(self.pos..end).ok_or(PrcError::Truncated(what))?;
|
||||
self.pos = end;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn u32(&mut self, what: &'static str) -> Result<u32, PrcError> {
|
||||
let s = self.take(4, what)?;
|
||||
let b: [u8; 4] = s.try_into().map_err(|_| PrcError::Truncated(what))?;
|
||||
Ok(u32::from_le_bytes(b))
|
||||
}
|
||||
|
||||
fn id(&mut self, what: &'static str) -> Result<UniqueId, PrcError> {
|
||||
Ok(UniqueId([self.u32(what)?, self.u32(what)?, self.u32(what)?, self.u32(what)?]))
|
||||
}
|
||||
|
||||
fn magic(&mut self) -> Result<(), PrcError> {
|
||||
match self.take(3, "magic") {
|
||||
Ok(b"PRC") => Ok(()),
|
||||
_ => Err(PrcError::NotPrc),
|
||||
}
|
||||
}
|
||||
|
||||
/// `UncompressedBlock`s: a count, then size-prefixed blocks [WD 10.2.2-3], each charged to
|
||||
/// `budget` before it is copied.
|
||||
fn blocks(&mut self, what: &'static str, budget: &mut Budget) -> Result<Vec<Vec<u8>>, PrcError> {
|
||||
let n = offset(self.u32(what)?);
|
||||
// Each block needs at least its 4-byte size, so `n` is bounded by the data left.
|
||||
if n > self.data.len().saturating_sub(self.pos) / 4 {
|
||||
return Err(PrcError::Truncated(what));
|
||||
}
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
let len = offset(self.u32(what)?);
|
||||
let block = self.take(len, what)?;
|
||||
budget.charge(block.len())?;
|
||||
Ok(block.to_vec())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored `u32` offset or count as a `usize` (saturating on a 16-bit target, where no data
|
||||
/// that large could exist).
|
||||
pub(crate) fn offset(v: u32) -> usize {
|
||||
usize::try_from(v).unwrap_or(usize::MAX)
|
||||
}
|
||||
|
||||
/// One file structure description of the file header [WD 6.2]: its id and the offsets of
|
||||
/// its sections.
|
||||
fn file_structure_description(r: &mut Bytes) -> Result<(UniqueId, Vec<usize>), PrcError> {
|
||||
let id = r.id("file structure description")?;
|
||||
if r.u32("file structure description")? != 0 {
|
||||
return Err(PrcError::Malformed("file structure reserved field is not 0".into()));
|
||||
}
|
||||
let count = offset(r.u32("file structure description")?);
|
||||
if count < SECTIONS {
|
||||
return Err(PrcError::Malformed(format!("{count} sections in a file structure")));
|
||||
}
|
||||
let mut offs = Vec::with_capacity(SECTIONS);
|
||||
// Offsets past the six this reader uses are read and dropped; each read is bounded by
|
||||
// the data, so a huge `count` ends in `Truncated`.
|
||||
for i in 0..count {
|
||||
let o = r.u32("section offset")?;
|
||||
if i < SECTIONS {
|
||||
offs.push(offset(o));
|
||||
}
|
||||
}
|
||||
Ok((id, offs))
|
||||
}
|
||||
|
||||
/// Where one file structure's parts lie: its section offsets and the end of its last section.
|
||||
struct Extent<'a> {
|
||||
offs: &'a [usize],
|
||||
last: usize,
|
||||
}
|
||||
|
||||
impl Extent<'_> {
|
||||
fn bound(&self, i: usize) -> usize {
|
||||
self.offs.get(i).copied().unwrap_or(self.last)
|
||||
}
|
||||
}
|
||||
|
||||
/// The file structure `id` lying at `extent`: its header and its inflated sections, charged
|
||||
/// to `budget`.
|
||||
fn file_structure(data: &[u8], id: UniqueId, extent: &Extent, budget: &mut Budget) -> Result<FileStructure, PrcError> {
|
||||
if extent.offs.windows(2).any(|w| w.first() > w.get(1)) || extent.bound(SECTIONS - 1) > extent.last {
|
||||
return Err(PrcError::Malformed("file structure section offsets are not ascending".into()));
|
||||
}
|
||||
let header = data.get(..extent.bound(1)).ok_or(PrcError::Truncated("file structure header"))?;
|
||||
let mut h = Bytes { data: header, pos: extent.bound(0) };
|
||||
h.magic()?;
|
||||
let min_version_for_read = h.u32("file structure header")?;
|
||||
let authoring_version = h.u32("file structure header")?;
|
||||
if h.id("file structure header")? != id {
|
||||
return Err(PrcError::Malformed("file structure id differs from its description".into()));
|
||||
}
|
||||
let _application = h.id("file structure header")?;
|
||||
// Headers may overlap, so each structure's copy of its pictures is charged.
|
||||
let pictures = h.blocks("file structure pictures", budget)?;
|
||||
let mut sections: [Vec<u8>; 5] = Default::default();
|
||||
for (i, (slot, kind)) in sections.iter_mut().zip(SectionKind::ALL).enumerate() {
|
||||
let end = if i + 2 < SECTIONS { extent.bound(i + 2) } else { extent.last };
|
||||
*slot = budget.inflate(data, (extent.bound(i + 1), end), kind.name())?;
|
||||
}
|
||||
Ok(FileStructure { id, min_version_for_read, authoring_version, pictures, sections })
|
||||
}
|
||||
|
||||
/// What is left of the inflation ceiling, and the ceiling itself for the error.
|
||||
struct Budget {
|
||||
left: usize,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
impl Budget {
|
||||
/// The zlib stream in `data[range]`, its size charged to the budget.
|
||||
fn inflate(&mut self, data: &[u8], range: (usize, usize), section: &'static str) -> Result<Vec<u8>, PrcError> {
|
||||
let (start, end) = range;
|
||||
if start == end {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let src = data.get(start..end).ok_or_else(|| PrcError::Malformed(format!("{section} section {start}..{end} is outside the data")))?;
|
||||
let mut out = Vec::new();
|
||||
// One byte past the budget is enough to know it was exceeded.
|
||||
let cap = u64::try_from(self.left).unwrap_or(u64::MAX).saturating_add(1);
|
||||
flate2::read::ZlibDecoder::new(src).take(cap).read_to_end(&mut out).map_err(|e| PrcError::Inflate { section, reason: e.to_string() })?;
|
||||
self.charge(out.len())?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Spend `n` bytes of the ceiling.
|
||||
fn charge(&mut self, n: usize) -> Result<(), PrcError> {
|
||||
self.left = self.left.checked_sub(n).ok_or(PrcError::TooLarge { limit: self.limit })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PrcFile {
|
||||
/// Parse a PRC stream (the decoded data of a `/3D` stream with `/Subtype /PRC`).
|
||||
///
|
||||
/// Newer `min_version_for_read` values are not refused: the fields read here are
|
||||
/// unchanged across published versions, and the caller sees the versions in
|
||||
/// [`PrcHeader`].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::NotPrc`] without the magic; [`PrcError::Truncated`] or
|
||||
/// [`PrcError::Malformed`] for a damaged header or offset table; [`PrcError::Inflate`]
|
||||
/// for a section that is not zlib; [`PrcError::TooLarge`] past [`MAX_INFLATED_BYTES`]
|
||||
/// in total, inflated sections and copied blocks together.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use pdfcraft_prc::{PrcError, PrcFile};
|
||||
/// assert_eq!(PrcFile::parse(b"%PDF"), Err(PrcError::NotPrc));
|
||||
/// ```
|
||||
pub fn parse(data: &[u8]) -> Result<Self, PrcError> {
|
||||
Self::parse_with_limit(data, MAX_INFLATED_BYTES)
|
||||
}
|
||||
|
||||
/// [`Self::parse`] with a caller-chosen ceiling on the total inflated size, for callers
|
||||
/// with less memory to spend than [`MAX_INFLATED_BYTES`].
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Self::parse`]; [`PrcError::TooLarge`] carries `limit`.
|
||||
pub fn parse_with_limit(data: &[u8], limit: usize) -> Result<Self, PrcError> {
|
||||
let mut r = Bytes { data, pos: 0 };
|
||||
r.magic()?;
|
||||
let header = PrcHeader {
|
||||
min_version_for_read: r.u32("file header")?,
|
||||
authoring_version: r.u32("file header")?,
|
||||
file_id: r.id("file header")?,
|
||||
application_id: r.id("file header")?,
|
||||
};
|
||||
let n_fs = offset(r.u32("file structure count")?);
|
||||
if n_fs == 0 || n_fs > MAX_FILE_STRUCTURES {
|
||||
return Err(PrcError::Malformed(format!("{n_fs} file structures")));
|
||||
}
|
||||
let descriptions = (0..n_fs).map(|_| file_structure_description(&mut r)).collect::<Result<Vec<_>, _>>()?;
|
||||
let mf_start = offset(r.u32("model file offsets")?);
|
||||
let mf_end = offset(r.u32("model file offsets")?);
|
||||
let mut budget = Budget { left: limit, limit };
|
||||
let uncompressed_files = r.blocks("uncompressed files", &mut budget)?;
|
||||
if mf_end < mf_start {
|
||||
return Err(PrcError::Malformed("model file ends before it starts".into()));
|
||||
}
|
||||
if mf_end > data.len() {
|
||||
return Err(PrcError::Malformed(format!("model file ends at {mf_end}, past the {}-byte stream", data.len())));
|
||||
}
|
||||
|
||||
// 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<_>, _>>()?;
|
||||
let model_file = budget.inflate(data, (mf_start, mf_end), "model file")?;
|
||||
Ok(PrcFile { header, file_structures, model_file, uncompressed_files })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//! What every section reader shares: the cursor, the schema, the authoring version, and the
|
||||
//! base fields every PRC entity starts with [WD 7.2.3, 7.4.3, 8.6].
|
||||
|
||||
use crate::PrcError;
|
||||
use crate::bits::BitReader;
|
||||
use crate::schema::Schema;
|
||||
use crate::tree::Graphics;
|
||||
|
||||
const ATTRIBUTE: u32 = 201;
|
||||
|
||||
/// A [`PrcError::Malformed`] naming `what`.
|
||||
pub(crate) fn malformed(what: String) -> PrcError {
|
||||
PrcError::Malformed(what)
|
||||
}
|
||||
|
||||
/// A section reader.
|
||||
pub(crate) struct Ctx<'a, 's> {
|
||||
pub(crate) r: BitReader<'a>,
|
||||
pub(crate) schema: &'s Schema,
|
||||
/// The file structure's authoring version; gates version-added fields.
|
||||
pub(crate) version: u32,
|
||||
/// 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, uid: 0, graphics: Graphics::default() }
|
||||
}
|
||||
|
||||
/// Reads an entity type code; any value but `want` is malformed.
|
||||
pub(crate) fn expect_type(&mut self, want: u32) -> Result<(), PrcError> {
|
||||
let t = self.r.unsigned_integer()?;
|
||||
if t != want {
|
||||
return Err(malformed(format!("entity type {t} where {want} belongs")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A count that must fit in the remaining data at `min_bits` per item, which bounds any
|
||||
/// allocation sized by it.
|
||||
pub(crate) fn count(&mut self, min_bits: usize, what: &'static str) -> Result<usize, PrcError> {
|
||||
let n = self.r.unsigned_integer()? as usize;
|
||||
if n.saturating_mul(min_bits) > self.r.remaining() {
|
||||
return Err(PrcError::Truncated(what));
|
||||
}
|
||||
self.r.take_elements(n)?;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// A counted array of `UnsignedInteger`s.
|
||||
pub(crate) fn uints(&mut self, what: &'static str) -> Result<Vec<u32>, PrcError> {
|
||||
let n = self.count(1, what)?;
|
||||
(0..n).map(|_| self.r.unsigned_integer()).collect()
|
||||
}
|
||||
|
||||
/// A counted array of `Double`s.
|
||||
pub(crate) fn doubles(&mut self, what: &'static str) -> Result<Vec<f64>, PrcError> {
|
||||
let n = self.count(2, what)?;
|
||||
(0..n).map(|_| self.r.double()).collect()
|
||||
}
|
||||
|
||||
/// `AttributeEntry` [WD 7.4.3]: a predefined title code or a string.
|
||||
fn attribute_entry(&mut self) -> Result<(), PrcError> {
|
||||
if self.r.bit()? {
|
||||
self.r.unsigned_integer()?;
|
||||
} else {
|
||||
self.r.string()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One attribute value [WD 7.4.3], read past.
|
||||
fn attribute_value(&mut self) -> Result<(), PrcError> {
|
||||
match self.r.unsigned_integer()? {
|
||||
0 => {}
|
||||
1 | 3 => {
|
||||
self.r.integer()?;
|
||||
}
|
||||
2 => {
|
||||
self.r.double()?;
|
||||
}
|
||||
4 => {
|
||||
self.r.string()?;
|
||||
}
|
||||
// prc-rs: a 64-bit integer is its high part, then its low part.
|
||||
5 => {
|
||||
self.r.integer()?;
|
||||
self.r.unsigned_integer()?;
|
||||
}
|
||||
k => return Err(malformed(format!("attribute value kind {k}"))),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `ContentPRCBase` [WD 7.2.3]: attributes, then the name; returns the entity's name,
|
||||
/// which is also the current name afterwards.
|
||||
pub(crate) fn content_prc_base(&mut self) -> Result<Option<String>, PrcError> {
|
||||
let n = self.count(1, "attributes")?;
|
||||
for _ in 0..n {
|
||||
self.expect_type(ATTRIBUTE)?;
|
||||
self.attribute_entry()?;
|
||||
let pairs = self.count(2, "attribute pairs")?;
|
||||
for _ in 0..pairs {
|
||||
self.attribute_entry()?;
|
||||
self.attribute_value()?;
|
||||
}
|
||||
self.schema.skip_added_fields(ATTRIBUTE, &mut self.r)?;
|
||||
}
|
||||
// `same_name` TRUE reuses the current name [WD 7.2.3.4].
|
||||
if !self.r.bit()? {
|
||||
self.name = self.r.string()?;
|
||||
}
|
||||
Ok(self.name.clone())
|
||||
}
|
||||
|
||||
/// `UserData` [WD 8.6]: a bit count, then that many opaque bits.
|
||||
pub(crate) fn user_data(&mut self) -> Result<(), PrcError> {
|
||||
let n = self.r.unsigned_integer()? as usize;
|
||||
self.r.skip_bits(n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! The crate's error type.
|
||||
|
||||
/// Why a PRC stream could not be read.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum PrcError {
|
||||
/// The data does not start with the `PRC` magic [WD 6.1.1].
|
||||
#[error("not a PRC stream: missing the \"PRC\" magic")]
|
||||
NotPrc,
|
||||
/// A value ran past the end of its byte or bit stream.
|
||||
#[error("PRC data truncated while reading {0}")]
|
||||
Truncated(&'static str),
|
||||
/// A structural field holds a value the format forbids.
|
||||
#[error("malformed PRC: {0}")]
|
||||
Malformed(String),
|
||||
/// No `Double` code of up to 22 bits matched [WD 11.17].
|
||||
#[error("PRC Double: no code matches the next 22 bits")]
|
||||
UnknownDoubleCode,
|
||||
/// A section is not a valid zlib stream.
|
||||
#[error("PRC {section} section does not inflate: {reason}")]
|
||||
Inflate {
|
||||
/// The section's name.
|
||||
section: &'static str,
|
||||
/// The decoder's message.
|
||||
reason: String,
|
||||
},
|
||||
/// The inflated sections and copied blocks together exceed the ceiling
|
||||
/// ([`crate::MAX_INFLATED_BYTES`] or the caller's limit).
|
||||
#[error("PRC sections inflate past the {limit}-byte ceiling")]
|
||||
TooLarge {
|
||||
/// The ceiling that was hit.
|
||||
limit: usize,
|
||||
},
|
||||
/// The arrays read from one stream hold more elements than [`crate::MAX_DECODED_ELEMENTS`].
|
||||
/// One bit can decode to a 4- or 8-byte element, so the inflation ceiling alone does not
|
||||
/// bound the memory the arrays take.
|
||||
#[error("PRC arrays decode past the {limit}-element ceiling")]
|
||||
TooManyElements {
|
||||
/// The ceiling that was hit.
|
||||
limit: usize,
|
||||
},
|
||||
/// A construct this reader does not decode; the part of the file holding it cannot be
|
||||
/// read past.
|
||||
#[error("PRC {0} is not supported")]
|
||||
Unsupported(&'static str),
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//! 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))
|
||||
}
|
||||
|
||||
/// The determinant of the whole matrix, by expansion along the last row.
|
||||
fn det4(&self) -> f64 {
|
||||
(0..4)
|
||||
.map(|j| {
|
||||
let cols: Vec<usize> = (0..4).filter(|&c| c != j).collect();
|
||||
let a = |i: usize, k: usize| cols.get(k).map_or(0.0, |&c| self.at(i, c));
|
||||
let minor = 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));
|
||||
let sign = if (3 + j) % 2 == 0 { 1.0 } else { -1.0 };
|
||||
sign * self.at(3, j) * minor
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Whether mapping points through this matrix turns them inside out. Where the point is
|
||||
/// divided by `w`, the Jacobian's sign is the sign of the 4×4 determinant; a last row of
|
||||
/// zeros (no division) falls back to the 3×3 block. Each row is first divided by its
|
||||
/// largest entry, which keeps both signs while the products stay in range.
|
||||
fn mirrors(&self) -> bool {
|
||||
let scaled = Matrix(self.0.map(|row| {
|
||||
let big = row.iter().fold(0.0_f64, |m, c| m.max(c.abs()));
|
||||
if big.is_finite() && big > 0.0 { row.map(|c| c / big) } else { row }
|
||||
}));
|
||||
let det = scaled.det4();
|
||||
if det == 0.0 { scaled.det3() < 0.0 } else { det < 0.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 4×4 determinant, which a
|
||||
/// negative `w` can cause too) reverses each triangle so the outside stays
|
||||
/// counter-clockwise. A projective `m` (a last row other than
|
||||
/// `[0, 0, 0, w]`) bends the surface per point, so the stored normals are dropped.
|
||||
///
|
||||
/// ```
|
||||
/// 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 mirrors = m.mirrors();
|
||||
let sign = if mirrors { -1.0 } else { 1.0 };
|
||||
if m.at(3, 0) != 0.0 || m.at(3, 1) != 0.0 || m.at(3, 2) != 0.0 {
|
||||
out.normals.clear();
|
||||
out.triangle_normals.clear();
|
||||
}
|
||||
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 mirrors {
|
||||
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]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_projective_transform_drops_the_stored_normals() {
|
||||
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, 1.0]],
|
||||
triangle_normals: vec![[0, 0, 0]],
|
||||
..TriangleMesh::default()
|
||||
};
|
||||
let mut projective = Matrix::IDENTITY;
|
||||
projective.0[3][0] = 1.0;
|
||||
let placed = mesh.transformed(&projective);
|
||||
assert!(placed.normals.is_empty() && placed.triangle_normals.is_empty());
|
||||
// A uniform w keeps them.
|
||||
let mut uniform = Matrix::IDENTITY;
|
||||
uniform.0[3][3] = 2.0;
|
||||
assert_eq!(mesh.transformed(&uniform).triangle_normals, [[0, 0, 0]]);
|
||||
}
|
||||
|
||||
/// A negative `w` turns the mapped triangles inside out though the 3×3 block does not
|
||||
/// mirror, so the winding is reversed by the sign of the whole determinant.
|
||||
#[test]
|
||||
fn a_negative_w_reverses_the_winding() {
|
||||
let mesh = TriangleMesh {
|
||||
positions: vec![[1.0, 0.0, 0.0], [0.0, 1.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 mut negated = Matrix::IDENTITY;
|
||||
negated.0[3][3] = -1.0;
|
||||
let placed = mesh.transformed(&negated);
|
||||
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]]);
|
||||
assert_eq!(placed.normals, [[-1.0, 0.0, 0.0]]);
|
||||
// A projective w that is negative over the mesh: w = z - 3.
|
||||
let mut projective = Matrix::IDENTITY;
|
||||
projective.0[3][2] = 1.0;
|
||||
projective.0[3][3] = -3.0;
|
||||
assert_eq!(mesh.transformed(&projective).triangles, [[0, 2, 1]]);
|
||||
// A positive w, uniform or projective, keeps the winding.
|
||||
let mut positive = Matrix::IDENTITY;
|
||||
positive.0[3][2] = 1.0;
|
||||
positive.0[3][3] = 3.0;
|
||||
assert_eq!(mesh.transformed(&positive).triangles, [[0, 1, 2]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mirror_whose_determinant_underflows_still_reverses_the_winding() {
|
||||
let mesh = TriangleMesh {
|
||||
positions: vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
|
||||
triangles: vec![[0, 1, 2]],
|
||||
..TriangleMesh::default()
|
||||
};
|
||||
let diagonal = |x: f64| {
|
||||
let mut m = Matrix::IDENTITY;
|
||||
(m.0[0][0], m.0[1][1], m.0[2][2]) = (x, 1e-200, 1e-200);
|
||||
m
|
||||
};
|
||||
assert_eq!(mesh.transformed(&diagonal(-1e200)).triangles, [[0, 2, 1]]);
|
||||
assert_eq!(mesh.transformed(&diagonal(1e200)).triangles, [[0, 1, 2]]);
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn transformed_normal(&self, mesh: &TriangleMesh) -> [f64; 3] {
|
||||
mesh.transformed(self).normals.first().copied().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! A reader for PRC, the 3D model format a PDF `/3D` stream of `/Subtype /PRC` carries
|
||||
//! (ISO 32000-2 §13.6.3, ISO 14739-1).
|
||||
//!
|
||||
//! 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; 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.
|
||||
//!
|
||||
//! Every read is bounded: inflation by [`MAX_INFLATED_BYTES`] (or a caller's limit), decoded
|
||||
//! array elements by [`MAX_DECODED_ELEMENTS`] and schema-program steps by [`MAX_SCHEMA_STEPS`]
|
||||
//! per reading pass, schema programs by a depth ceiling. Damaged data is an error, never a panic.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
mod acof;
|
||||
mod arrays;
|
||||
mod bits;
|
||||
mod container;
|
||||
mod entity;
|
||||
mod error;
|
||||
mod geometry;
|
||||
mod schema;
|
||||
mod tess;
|
||||
#[cfg(test)]
|
||||
mod testw;
|
||||
mod tree;
|
||||
|
||||
pub use bits::{BitReader, MAX_DECODED_ELEMENTS, MAX_SCHEMA_STEPS};
|
||||
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};
|
||||
@@ -0,0 +1,565 @@
|
||||
//! `FileStructureSchema` and its token interpreter [WD 6.3, 9].
|
||||
//!
|
||||
//! A producer newer than the reader describes, per entity type, the fields it appended; the
|
||||
//! reader runs that program after the fields it knows, to step over the rest [WD 9.1]. Token
|
||||
//! values follow the WD 9.2 table as corrected by PDF Association pdf-issues #407
|
||||
//! (`Block_Start` = 19; 39 and 40 swallow one token).
|
||||
//!
|
||||
//! Choices the sources leave open, made here:
|
||||
//! - `SimpleFor` reads its count as an `UnsignedInteger` [WD 9.3.17 pseudocode; pdf-issues
|
||||
//! #575]: that read also decodes an `Integer`-written non-negative count, while a signed
|
||||
//! read does not decode an unsigned one whose top group is 0x80 or higher.
|
||||
//! - `Block_Version V` runs its block only when `V` is newer than
|
||||
//! [`crate::PRC_READER_VERSION`]: an older block holds fields this reader already reads
|
||||
//! natively [WD 9.3.20].
|
||||
//! - `Extent_1D` and `Extent_2D` read 2 and 4 `Double`s: `Interval` and `Domain`
|
||||
//! [WD 8.2.2, 8.2.4].
|
||||
//! - Expression values are `i64`; a `Double` read as an operand truncates (NaN reads as 0).
|
||||
//! - Variables are not block-scoped: a program that reuses an index after its block ends
|
||||
//! reads the stale value instead of an error.
|
||||
//! - Pointer tokens (12-14) and `Value_CurveIs3D` (28) need the exact-geometry reader and are
|
||||
//! refused with [`PrcError::Unsupported`].
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::PrcError;
|
||||
use crate::bits::BitReader;
|
||||
|
||||
/// Nesting depth of blocks, loops, expressions and `Father_Type` calls.
|
||||
const MAX_DEPTH: u32 = 64;
|
||||
|
||||
/// The per-entity-type programs at the start of a globals or model-file
|
||||
/// bitstream [WD 6.3.1].
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Schema {
|
||||
programs: HashMap<u32, Vec<u32>>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
/// Read a schema: `UnsignedInteger` count, then per entry the entity
|
||||
/// type, a token count and the tokens [WD 6.3.2].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Truncated`] when a count runs past the data;
|
||||
/// [`PrcError::Malformed`] for an entity type listed twice; [`PrcError::TooManyElements`]
|
||||
/// past the reader's allowance, one element per entry and per token.
|
||||
pub fn read(r: &mut BitReader<'_>) -> Result<Self, PrcError> {
|
||||
let n = r.unsigned_integer()? as usize;
|
||||
// Every entry takes at least two bits.
|
||||
if n > r.remaining() / 2 {
|
||||
return Err(PrcError::Truncated("the schema"));
|
||||
}
|
||||
let mut programs = HashMap::new();
|
||||
for _ in 0..n {
|
||||
// The entry itself costs one element, so empty programs are not free.
|
||||
r.take_elements(1)?;
|
||||
let entity_type = r.unsigned_integer()?;
|
||||
let len = r.unsigned_integer()? as usize;
|
||||
if len > r.remaining() {
|
||||
return Err(PrcError::Truncated("a schema program"));
|
||||
}
|
||||
r.take_elements(len)?;
|
||||
let tokens = (0..len).map(|_| r.unsigned_integer()).collect::<Result<Vec<_>, _>>()?;
|
||||
if programs.insert(entity_type, tokens).is_some() {
|
||||
return Err(PrcError::Malformed(format!("schema lists entity type {entity_type} twice")));
|
||||
}
|
||||
}
|
||||
Ok(Self { programs })
|
||||
}
|
||||
|
||||
/// True when no entity type has added fields (the common case: the file
|
||||
/// was written at the version this reader implements).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.programs.is_empty()
|
||||
}
|
||||
|
||||
/// The entity types the schema describes, unordered.
|
||||
pub fn entity_types(&self) -> impl Iterator<Item = u32> + '_ {
|
||||
self.programs.keys().copied()
|
||||
}
|
||||
|
||||
/// Consume the fields a newer producer appended to `entity_type`, if the
|
||||
/// schema describes it; a no-op otherwise. Each step is charged to `r`, so every
|
||||
/// program run on one stream shares [`crate::MAX_SCHEMA_STEPS`].
|
||||
///
|
||||
/// # Errors
|
||||
/// [`PrcError::Unsupported`] for a token that needs exact geometry;
|
||||
/// [`PrcError::Malformed`] for an ill-formed program, one past the depth ceiling, or
|
||||
/// one that runs out of the stream's steps; any read error from `r`.
|
||||
pub fn skip_added_fields(&self, entity_type: u32, r: &mut BitReader<'_>) -> Result<(), PrcError> {
|
||||
let Some(program) = self.programs.get(&entity_type) else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut exec = Exec { schema: self, r, vars: HashMap::new(), loop_counter: 0 };
|
||||
exec.program(program, 0)
|
||||
}
|
||||
}
|
||||
|
||||
struct Exec<'s, 'r, 'd> {
|
||||
schema: &'s Schema,
|
||||
r: &'r mut BitReader<'d>,
|
||||
vars: HashMap<u32, i64>,
|
||||
loop_counter: i64,
|
||||
}
|
||||
|
||||
fn malformed(what: &str) -> PrcError {
|
||||
PrcError::Malformed(format!("schema program: {what}"))
|
||||
}
|
||||
|
||||
/// The token at `*pos`, advancing past it.
|
||||
fn next(prog: &[u32], pos: &mut usize) -> Result<u32, PrcError> {
|
||||
let t = prog.get(*pos).copied().ok_or_else(|| malformed("ends mid-statement"))?;
|
||||
*pos += 1;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
impl Exec<'_, '_, '_> {
|
||||
fn tick(&mut self, depth: u32) -> Result<(), PrcError> {
|
||||
if !self.r.take_step() {
|
||||
return Err(malformed("step ceiling reached"));
|
||||
}
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(malformed("nesting ceiling reached"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn program(&mut self, prog: &[u32], depth: u32) -> Result<(), PrcError> {
|
||||
let mut pos = 0;
|
||||
while pos < prog.len() {
|
||||
self.stmt(prog, &mut pos, true, depth)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One data read for tokens 0-5 and 7-11, returning its value as an
|
||||
/// operand.
|
||||
fn read(&mut self, token: u32) -> Result<i64, PrcError> {
|
||||
let doubles = match token {
|
||||
0 => return Ok(i64::from(self.r.bit()?)),
|
||||
1 => return Ok(self.r.double()? as i64),
|
||||
2 => return Ok(i64::from(self.r.character()?)),
|
||||
3 => return Ok(i64::from(self.r.unsigned_integer()?)),
|
||||
4 => return Ok(i64::from(self.r.integer()?)),
|
||||
5 => {
|
||||
self.r.string()?;
|
||||
return Ok(0);
|
||||
}
|
||||
7 => 2,
|
||||
8 => 3,
|
||||
9 => 2,
|
||||
10 => 4,
|
||||
11 => 6,
|
||||
_ => return Err(malformed("not a data token")),
|
||||
};
|
||||
for _ in 0..doubles {
|
||||
self.r.double()?;
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Statements until `Block_End`, which is consumed.
|
||||
fn block(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
|
||||
loop {
|
||||
if prog.get(*pos) == Some(&21) {
|
||||
*pos += 1;
|
||||
return Ok(());
|
||||
}
|
||||
self.stmt(prog, pos, run, depth + 1)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `body` (one statement at `*pos`) `count` times; `*pos` ends past it.
|
||||
fn repeat(&mut self, prog: &[u32], pos: &mut usize, count: i64, run: bool, depth: u32) -> Result<(), PrcError> {
|
||||
let start = *pos;
|
||||
self.stmt(prog, pos, false, depth + 1)?;
|
||||
if run {
|
||||
let saved = self.loop_counter;
|
||||
for i in 0..count.max(0) {
|
||||
self.tick(depth)?;
|
||||
self.loop_counter = i;
|
||||
let mut p = start;
|
||||
self.stmt(prog, &mut p, true, depth + 1)?;
|
||||
}
|
||||
self.loop_counter = saved;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One statement; with `run` false, only step over its tokens.
|
||||
fn stmt(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
|
||||
self.tick(depth)?;
|
||||
match next(prog, pos)? {
|
||||
t @ (0..=5 | 7..=11) => {
|
||||
if run {
|
||||
self.read(t)?;
|
||||
}
|
||||
}
|
||||
6 => self.parent_program(prog, pos, run, depth)?,
|
||||
12 => {
|
||||
next(prog, pos)?;
|
||||
if run {
|
||||
return Err(PrcError::Unsupported("a schema pointer field"));
|
||||
}
|
||||
}
|
||||
13 | 14 => {
|
||||
if run {
|
||||
return Err(PrcError::Unsupported("a schema pointer field"));
|
||||
}
|
||||
}
|
||||
15 => {
|
||||
let count = self.expr(prog, pos, run, depth + 1)?;
|
||||
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)?;
|
||||
}
|
||||
17 => self.conditional(prog, pos, run, depth)?,
|
||||
19 => self.block(prog, pos, run, depth)?,
|
||||
20 => {
|
||||
let version = next(prog, pos)?;
|
||||
let newer = version > crate::PRC_READER_VERSION;
|
||||
self.block(prog, pos, run && newer, depth)?;
|
||||
}
|
||||
22 => {
|
||||
let v = next(prog, pos)?;
|
||||
if run {
|
||||
self.vars.insert(v, 0);
|
||||
}
|
||||
}
|
||||
23 | 24 => {
|
||||
let v = next(prog, pos)?;
|
||||
let value = self.expr(prog, pos, run, depth + 1)?;
|
||||
if run {
|
||||
self.vars.insert(v, value);
|
||||
}
|
||||
}
|
||||
39 | 40 => {
|
||||
next(prog, pos)?;
|
||||
}
|
||||
t => return Err(malformed(&format!("token {t} where a statement belongs"))),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Token 6: run the named type's own program, the fields it inherits.
|
||||
fn parent_program(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
|
||||
let parent = next(prog, pos)?;
|
||||
if run && let Some(p) = self.schema.programs.get(&parent) {
|
||||
self.tick(depth + 1)?;
|
||||
let mut q = 0;
|
||||
while q < p.len() {
|
||||
self.stmt(p, &mut q, true, depth + 1)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Token 17: `if` with an optional token-18 `else`.
|
||||
fn conditional(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<(), PrcError> {
|
||||
let cond = self.expr(prog, pos, run, depth + 1)? != 0;
|
||||
self.stmt(prog, pos, run && cond, depth + 1)?;
|
||||
if prog.get(*pos) == Some(&18) {
|
||||
*pos += 1;
|
||||
self.stmt(prog, pos, run && !cond, depth + 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One expression; with `run` false its value is 0 and nothing is read.
|
||||
fn expr(&mut self, prog: &[u32], pos: &mut usize, run: bool, depth: u32) -> Result<i64, PrcError> {
|
||||
self.tick(depth)?;
|
||||
let t = next(prog, pos)?;
|
||||
match t {
|
||||
0..=4 => {
|
||||
if run {
|
||||
self.read(t)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
25 => {
|
||||
let v = next(prog, pos)?;
|
||||
if !run {
|
||||
return Ok(0);
|
||||
}
|
||||
self.vars.get(&v).copied().ok_or_else(|| malformed(&format!("variable {v} read before it is set")))
|
||||
}
|
||||
26 => Ok(i64::from(next(prog, pos)?)),
|
||||
27 => Ok(self.loop_counter),
|
||||
28 => {
|
||||
if run {
|
||||
Err(PrcError::Unsupported("Value_CurveIs3D"))
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
29..=38 => {
|
||||
let a = self.expr(prog, pos, run, depth + 1)?;
|
||||
let b = self.expr(prog, pos, run, depth + 1)?;
|
||||
if !run {
|
||||
return Ok(0);
|
||||
}
|
||||
let overflow = || malformed("a schema expression overflows");
|
||||
Ok(match t {
|
||||
29 => a.checked_mul(b).ok_or_else(overflow)?,
|
||||
30 => a.checked_div(b).ok_or_else(|| malformed("division by zero"))?,
|
||||
31 => a.checked_add(b).ok_or_else(overflow)?,
|
||||
32 => a.checked_sub(b).ok_or_else(overflow)?,
|
||||
33 => i64::from(a < b),
|
||||
34 => i64::from(a <= b),
|
||||
35 => i64::from(a > b),
|
||||
36 => i64::from(a >= b),
|
||||
37 => i64::from(a == b),
|
||||
_ => i64::from(a != b),
|
||||
})
|
||||
}
|
||||
t => Err(malformed(&format!("token {t} where a value belongs"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testw::W;
|
||||
|
||||
fn schema(entries: &[(u32, &[u32])]) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(entries.len() as u32);
|
||||
for (t, toks) in entries {
|
||||
w.uint(*t).uint(toks.len() as u32);
|
||||
for k in *toks {
|
||||
w.uint(*k);
|
||||
}
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// Each schema entry and its program's tokens are charged against the reader's element
|
||||
/// allowance.
|
||||
#[test]
|
||||
fn program_tokens_are_charged_against_the_allowance() {
|
||||
let bytes = schema(&[(172, &[1, 2, 3])]).bytes();
|
||||
assert!(matches!(Schema::read(&mut BitReader::with_elements(&bytes, 3)), Err(PrcError::TooManyElements { .. })));
|
||||
assert!(Schema::read(&mut BitReader::with_elements(&bytes, 4)).is_ok());
|
||||
let empty = schema(&[(172, &[]), (173, &[]), (174, &[])]).bytes();
|
||||
assert!(matches!(Schema::read(&mut BitReader::with_elements(&empty, 2)), Err(PrcError::TooManyElements { .. })));
|
||||
assert!(Schema::read(&mut BitReader::with_elements(&empty, 3)).is_ok());
|
||||
}
|
||||
|
||||
fn run(entries: &[(u32, &[u32])], data: &W) -> (Result<(), PrcError>, usize) {
|
||||
let mut w = schema(entries);
|
||||
w.append(data);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
let s = Schema::read(&mut r).unwrap();
|
||||
let start = r.position();
|
||||
let res = s.skip_added_fields(172, &mut r);
|
||||
(res, r.position() - start)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_string_and_boolean_tokens_consume_their_fields() {
|
||||
let mut d = W::default();
|
||||
d.bit(true).uint(300).int(-7).string(Some("ab")).put(0x41, 8);
|
||||
let want = d.len();
|
||||
d.uint(99); // not part of the added fields
|
||||
let (res, used) = run(&[(172, &[0, 3, 4, 5, 2])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_tokens_consume_their_fields() {
|
||||
let mut d = W::default();
|
||||
d.bit(true).double(1.5).uint(300).int(-7).string(Some("ab")).double(2.0).double(3.0);
|
||||
let want = d.len();
|
||||
d.uint(99); // not part of the added fields
|
||||
let (res, used) = run(&[(172, &[0, 1, 3, 4, 5, 7])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_extent_and_box_tokens_read_their_doubles() {
|
||||
// Vec3, Extent1D, Extent2D, BBox: 3 + 2 + 4 + 6 Doubles.
|
||||
let mut d = W::default();
|
||||
for i in 0..15 {
|
||||
d.double(f64::from(i) + 0.5);
|
||||
}
|
||||
let want = d.len();
|
||||
d.uint(99);
|
||||
let (res, used) = run(&[(172, &[8, 9, 10, 11])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_and_comparison_operators() {
|
||||
// For (5 - 3) { UInt }, then If (2 < 2) { Bool } Else { Char }.
|
||||
let mut d = W::default();
|
||||
d.uint(7).uint(8).put(0x41, 8);
|
||||
let want = d.len();
|
||||
d.uint(99);
|
||||
let prog = [15, 32, 26, 5, 26, 3, 3, 17, 33, 26, 2, 26, 2, 0, 18, 2];
|
||||
let (res, used) = run(&[(172, &prog)], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_absent_type_is_a_no_op() {
|
||||
let mut w = schema(&[(300, &[3])]);
|
||||
w.uint(5);
|
||||
let bytes = w.bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
let s = Schema::read(&mut r).unwrap();
|
||||
let at = r.position();
|
||||
s.skip_added_fields(172, &mut r).unwrap();
|
||||
assert_eq!(r.position(), at);
|
||||
assert!(!s.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_for_reads_its_count_then_repeats_the_body() {
|
||||
let mut d = W::default();
|
||||
d.int(3).uint(1).uint(2).uint(3);
|
||||
let want = d.len();
|
||||
d.uint(77);
|
||||
let (res, used) = run(&[(172, &[16, 3])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
|
||||
// A block body.
|
||||
let mut d = W::default();
|
||||
d.int(2).bit(true).uint(9).bit(false).uint(8);
|
||||
let want = d.len();
|
||||
let (res, used) = run(&[(172, &[16, 19, 0, 3, 21])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
|
||||
// A count whose top byte is 0x80 or higher, written either way.
|
||||
for signed in [false, true] {
|
||||
let mut d = W::default();
|
||||
if signed {
|
||||
d.int(200);
|
||||
} else {
|
||||
d.uint(200);
|
||||
}
|
||||
(0..200).for_each(|_| {
|
||||
d.bit(true);
|
||||
});
|
||||
let want = d.len();
|
||||
d.uint(77);
|
||||
let (res, used) = run(&[(172, &[16, 0])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want, "signed={signed}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_else_and_variables_choose_the_branch_by_file_data() {
|
||||
// DeclareAndSet v0 = <UInt from file>; if v0 > 1 { UInt } else { Character }.
|
||||
let prog: &[u32] = &[24, 0, 3, 17, 35, 25, 0, 26, 1, 3, 18, 2];
|
||||
let mut d = W::default();
|
||||
d.uint(5).uint(4250);
|
||||
let want = d.len();
|
||||
let (res, used) = run(&[(172, prog)], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
|
||||
let mut d = W::default();
|
||||
d.uint(0).put(0xAB, 8);
|
||||
let want = d.len();
|
||||
let (res, used) = run(&[(172, prog)], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_counts_by_expression_and_exposes_the_counter() {
|
||||
// For (2 * 2) { if counter == 2 { UInt } } => one UInt read.
|
||||
let prog: &[u32] = &[15, 29, 26, 2, 26, 2, 17, 37, 27, 26, 2, 3];
|
||||
let mut d = W::default();
|
||||
d.uint(1234);
|
||||
let want = d.len();
|
||||
let (res, used) = run(&[(172, prog)], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_expression_that_overflows_is_malformed() {
|
||||
// For ((2^31 * 2^31) * 4) { UInt }: 2^64 would wrap to 0 and read nothing.
|
||||
let prog: &[u32] = &[15, 29, 29, 26, 1 << 31, 26, 1 << 31, 26, 4, 3];
|
||||
let (res, _) = run(&[(172, prog)], &W::default());
|
||||
assert!(matches!(res, Err(PrcError::Malformed(m)) if m.contains("overflows")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_blocks_run_only_when_newer_than_the_reader() {
|
||||
let mut d = W::default();
|
||||
d.uint(42);
|
||||
let want = d.len();
|
||||
let (res, used) = run(&[(172, &[20, 8137, 3, 21, 20, 9000, 3, 21])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn father_type_runs_the_parent_program_and_obsolete_tokens_swallow_one() {
|
||||
let mut d = W::default();
|
||||
d.uint(1).uint(2);
|
||||
let want = d.len();
|
||||
let (res, used) = run(&[(172, &[39, 3, 6, 50, 40, 8]), (50, &[3, 3])], &d);
|
||||
res.unwrap();
|
||||
assert_eq!(used, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_programs_are_refused_not_run_forever() {
|
||||
let d = W::default();
|
||||
// For (1000000 * 1000000) { empty block }.
|
||||
let prog: &[u32] = &[15, 29, 26, 1_000_000, 26, 1_000_000, 19, 21];
|
||||
assert!(matches!(run(&[(172, prog)], &d).0, Err(PrcError::Malformed(_))));
|
||||
// Father_Type recursing into itself.
|
||||
assert!(matches!(run(&[(172, &[6, 172])], &d).0, Err(PrcError::Malformed(_))));
|
||||
// A pointer field.
|
||||
assert!(matches!(run(&[(172, &[13])], &d).0, Err(PrcError::Unsupported(_))));
|
||||
// A stray Block_End, an unterminated block, a division by zero.
|
||||
assert!(matches!(run(&[(172, &[21])], &d).0, Err(PrcError::Malformed(_))));
|
||||
assert!(matches!(run(&[(172, &[19, 0])], &d).0, Err(PrcError::Malformed(_))));
|
||||
assert!(matches!(run(&[(172, &[23, 0, 30, 26, 1, 26, 0])], &d).0, Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
/// Every program run on one reader draws on one step allowance.
|
||||
#[test]
|
||||
fn programs_on_one_stream_share_the_step_allowance() {
|
||||
// For (2^19) { empty block }: about 2^20 steps, so the 2^22 allowance fits three runs.
|
||||
let bytes = schema(&[(172, &[15, 26, 1 << 19, 19, 21])]).bytes();
|
||||
let mut r = BitReader::new(&bytes);
|
||||
let s = Schema::read(&mut r).unwrap();
|
||||
let runs = (0..5).take_while(|_| s.skip_added_fields(172, &mut r).is_ok()).count();
|
||||
assert_eq!(runs, 3);
|
||||
assert!(matches!(s.skip_added_fields(172, &mut r), Err(PrcError::Malformed(m)) if m.contains("step ceiling")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_duplicated_entity_type_is_malformed() {
|
||||
let bytes = schema(&[(172, &[3]), (172, &[3])]).bytes();
|
||||
assert!(matches!(Schema::read(&mut BitReader::new(&bytes)), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_entry_count_past_the_data_is_refused_before_any_entry_is_read() {
|
||||
let mut w = W::default();
|
||||
w.uint(1_000).uint(172).uint(0);
|
||||
let bytes = w.bytes();
|
||||
assert_eq!(Schema::read(&mut BitReader::new(&bytes)).unwrap_err(), PrcError::Truncated("the schema"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
//! The tessellation section: `FileStructureTessellation` (305) and its entities, `TESS_3D`
|
||||
//! (172) with its `TESS_Face`s (174), `TESS_3D_Wire` (175), `TESS_Markup` (176) and
|
||||
//! `TESS_3D_Compressed` (173) [WD 7.3.7, 7.8].
|
||||
//!
|
||||
//! `TESS_3D_Compressed` is read field by field to its end [WD 7.8.9.7] so the entities after
|
||||
//! it decode; its triangles are not rebuilt.
|
||||
//!
|
||||
//! The schema runs after the fields of each concrete type read here (201, 172, 173, 174, 175,
|
||||
//! 176, 305), not for the abstract levels (`PRCBase`, `TESS`).
|
||||
|
||||
mod triangulate;
|
||||
|
||||
use crate::arrays;
|
||||
use crate::bits::{BitReader, Budget};
|
||||
use crate::entity::{Ctx, malformed};
|
||||
use crate::{FileStructure, PrcError, SectionKind};
|
||||
use triangulate::{Corners, Face, triangle_elements};
|
||||
|
||||
const FILE_STRUCTURE_TESSELLATION: u32 = 305;
|
||||
const TESS_3D: u32 = 172;
|
||||
const TESS_3D_COMPRESSED: u32 = 173;
|
||||
const TESS_FACE: u32 = 174;
|
||||
const TESS_3D_WIRE: u32 = 175;
|
||||
const TESS_MARKUP: u32 = 176;
|
||||
|
||||
/// prc-rs: `origin` exists in a compressed tessellation from this version (pdf-issues #705).
|
||||
const ORIGIN_FROM: u32 = 7031;
|
||||
/// prc-rs: `has_loops` exists from this file-structure authoring version.
|
||||
const HAS_LOOPS_FROM: u32 = 7039;
|
||||
/// prc-rs: `must_recalculate_normals` exists from this version.
|
||||
const RECALCULATE_FROM: u32 = 7047;
|
||||
|
||||
/// A wire header's count bits; the top four are flags [WD 7.8.7.5].
|
||||
const WIRE_COUNT_MASK: u32 = 0x0FFF_FFFF;
|
||||
const WIRE_IS_CLOSING: u32 = 0x1000_0000;
|
||||
const WIRE_IS_CONTINUOUS: u32 = 0x2000_0000;
|
||||
|
||||
/// Normal angles wider than this are not a `ShortArray` [WD 7.8.9.7].
|
||||
const MAX_ANGLE_BITS: u32 = 16;
|
||||
|
||||
/// One entry of the tessellation array; a representation item's `index_tessellation` selects
|
||||
/// one by position.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Tessellation {
|
||||
/// A `TESS_3D` triangle mesh.
|
||||
Mesh(TriangleMesh),
|
||||
/// A `TESS_3D_Wire`: polylines.
|
||||
Wire(Vec<Vec<[f64; 3]>>),
|
||||
/// A `TESS_Markup` (PMI drawing), read past but not decoded.
|
||||
Markup,
|
||||
/// A `TESS_3D_Compressed` mesh, read past but not rebuilt.
|
||||
#[non_exhaustive]
|
||||
Compressed {
|
||||
/// The entity's triangle count.
|
||||
triangles: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// A `TESS_3D` decoded to indexed triangles, in the entity's own frame (the representation
|
||||
/// item's placement is not applied).
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct TriangleMesh {
|
||||
/// The coordinate array as points.
|
||||
pub positions: Vec<[f64; 3]>,
|
||||
/// Counter-clockwise triangles seen from the outside, as indices into
|
||||
/// [`Self::positions`] [WD 7.8.5.2].
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
/// `triangles` split by `TESS_Face`: face `i` is `triangles[faces[i].clone()]`.
|
||||
pub faces: Vec<std::ops::Range<usize>>,
|
||||
/// The producer asks the reader to compute normals (none are stored).
|
||||
pub normals_recalculated: bool,
|
||||
/// The stored vertex normals [WD 7.8.5.1], as written (not renormalised); empty when the
|
||||
/// mesh stores none or any of its normal indices is unusable.
|
||||
pub normals: Vec<[f64; 3]>,
|
||||
/// Per triangle, the index into [`Self::normals`] of each corner's normal, in the corner
|
||||
/// order of [`Self::triangles`]; empty exactly when [`Self::normals`] is.
|
||||
pub triangle_normals: Vec<[u32; 3]>,
|
||||
/// Per triangle, the style its face's line attributes give it [WD 7.8.6]; empty when no
|
||||
/// face carries any.
|
||||
pub triangle_styles: Vec<TriangleStyle>,
|
||||
/// The stored texture coordinates as (u, v) pairs [WD 7.8.5.1]; empty when the mesh
|
||||
/// stores none.
|
||||
pub uvs: Vec<[f64; 2]>,
|
||||
/// Per texture-coordinate set, per triangle, the index into [`Self::uvs`] of each
|
||||
/// corner's coordinate, in the corner order of [`Self::triangles`]; each set is as long
|
||||
/// as [`Self::triangles`], `None` for a triangle whose face stores no coordinate for the
|
||||
/// set. Empty when [`Self::uvs`] is.
|
||||
pub triangle_uvs: Vec<Vec<Option<[u32; 3]>>>,
|
||||
}
|
||||
|
||||
/// The graphics a face's `line_attributes` give one triangle [WD 7.8.6].
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub struct TriangleStyle {
|
||||
/// An index into the globals' style array plus one; 0 keeps the owner's graphics.
|
||||
pub style: u32,
|
||||
/// The face's behaviour bits [WD 5.4].
|
||||
pub behaviour: u16,
|
||||
}
|
||||
|
||||
impl FileStructure {
|
||||
/// The tessellation section's entities, in the order a representation item's
|
||||
/// `index_tessellation` counts them [WD 7.3.7].
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`crate::Schema::read`] for the globals' schema; [`PrcError::Truncated`] or
|
||||
/// [`PrcError::Malformed`] for a damaged section; [`PrcError::Unsupported`] for
|
||||
/// optimised vertex colours.
|
||||
pub fn tessellations(&self) -> Result<Vec<Tessellation>, PrcError> {
|
||||
self.tessellations_within(Budget::FULL)
|
||||
}
|
||||
|
||||
/// As [`Self::tessellations`], the globals' schema and the section sharing `budget`.
|
||||
pub(crate) fn tessellations_within(&self, budget: Budget) -> Result<Vec<Tessellation>, PrcError> {
|
||||
let mut globals = BitReader::with_budget(self.section(SectionKind::Globals), budget);
|
||||
let schema = crate::Schema::read(&mut globals)?;
|
||||
let r = BitReader::with_budget(self.section(SectionKind::Tessellation), globals.budget());
|
||||
Ctx::new(r, &schema, self.authoring_version).file_structure_tessellation()
|
||||
}
|
||||
}
|
||||
|
||||
impl Ctx<'_, '_> {
|
||||
/// `FileStructureTessellation` (305) [WD 7.3.7].
|
||||
pub(crate) fn file_structure_tessellation(&mut self) -> Result<Vec<Tessellation>, PrcError> {
|
||||
self.expect_type(FILE_STRUCTURE_TESSELLATION)?;
|
||||
self.content_prc_base()?;
|
||||
let n = self.count(1, "tessellations")?;
|
||||
let mut out = Vec::with_capacity(n.min(1024));
|
||||
for _ in 0..n {
|
||||
out.push(match self.r.unsigned_integer()? {
|
||||
TESS_3D => Tessellation::Mesh(self.tess_3d()?),
|
||||
TESS_3D_WIRE => Tessellation::Wire(self.wire()?),
|
||||
TESS_MARKUP => {
|
||||
self.markup()?;
|
||||
Tessellation::Markup
|
||||
}
|
||||
TESS_3D_COMPRESSED => Tessellation::Compressed { triangles: self.tess_3d_compressed()? },
|
||||
t => return Err(malformed(format!("entity type {t} in the tessellation array"))),
|
||||
});
|
||||
}
|
||||
self.schema.skip_added_fields(FILE_STRUCTURE_TESSELLATION, &mut self.r)?;
|
||||
self.user_data()?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `ContentBaseTessData` [WD 7.8.4]: `is_calculated`, then coordinates.
|
||||
fn base_tess_data(&mut self) -> Result<Vec<f64>, PrcError> {
|
||||
self.r.bit()?;
|
||||
self.doubles("tessellation coordinates")
|
||||
}
|
||||
|
||||
/// `VertexColors` [WD 7.8.7.2] for `count` points, read past.
|
||||
fn vertex_colors(&mut self, count: usize, in_wire: bool) -> Result<(), PrcError> {
|
||||
let rgba = self.r.bit()?;
|
||||
// prc-rs: `is_segment_color` exists only inside a wire.
|
||||
let per_segment = in_wire && self.r.bit()?;
|
||||
if self.r.bit()? {
|
||||
return Err(PrcError::Unsupported("optimised vertex colours"));
|
||||
}
|
||||
let count = if per_segment { count / 2 } else { count };
|
||||
let bytes = if rgba { 4 } else { 3 };
|
||||
// The first colour is `8 * bytes` bits; every later one is at least its reuse bit.
|
||||
if count > 0 && (count - 1).saturating_add(8 * bytes) > self.r.remaining() {
|
||||
return Err(PrcError::Truncated("vertex colours"));
|
||||
}
|
||||
for i in 0..count {
|
||||
if i == 0 || !self.r.bit()? {
|
||||
for _ in 0..bytes {
|
||||
self.r.character()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `TESS_3D` (172) after its type code [WD 7.8.5].
|
||||
fn tess_3d(&mut self) -> Result<TriangleMesh, PrcError> {
|
||||
let coords = self.base_tess_data()?;
|
||||
let _has_faces = self.r.bit()?;
|
||||
if self.version >= HAS_LOOPS_FROM {
|
||||
let _has_loops = self.r.bit()?;
|
||||
}
|
||||
let recalc = self.version >= RECALCULATE_FROM && self.r.bit()?;
|
||||
if recalc {
|
||||
let _flags = self.r.character()?;
|
||||
let _crease_angle = self.r.double()?;
|
||||
}
|
||||
let normal_coords = self.doubles("normal coordinates")?;
|
||||
let _wire_indices = self.uints("wire indices")?;
|
||||
let indices = self.uints("triangulated indices")?;
|
||||
let n_faces = self.count(1, "faces")?;
|
||||
let mut faces = Vec::with_capacity(n_faces.min(1024));
|
||||
for _ in 0..n_faces {
|
||||
self.expect_type(TESS_FACE)?;
|
||||
faces.push(self.face()?);
|
||||
}
|
||||
let texture = self.doubles("texture coordinates")?;
|
||||
self.schema.skip_added_fields(TESS_3D, &mut self.r)?;
|
||||
|
||||
let mut mesh = TriangleMesh { positions: points(&coords)?, normals_recalculated: recalc, ..TriangleMesh::default() };
|
||||
let styled = faces.iter().any(|f| !f.styles.is_empty());
|
||||
let mut corners = Corners::default();
|
||||
// Every triangle carries a corner triple for each texture set any face uses.
|
||||
let sets = faces.iter().map(Face::texture_sets).max().unwrap_or(0);
|
||||
let per_triangle = triangle_elements(!recalc, sets, styled);
|
||||
for f in &faces {
|
||||
// Faces may reuse one index range, so each face's triangles, with what they
|
||||
// carry, are charged before they are made.
|
||||
self.r.take_elements(f.triangle_count()?.saturating_mul(per_triangle))?;
|
||||
let start = mesh.triangles.len();
|
||||
let entities = f.triangulate(&indices, !recalc, mesh.positions.len(), &mut mesh.triangles, &mut corners)?;
|
||||
mesh.faces.push(start..mesh.triangles.len());
|
||||
if styled {
|
||||
f.styles_into(&entities, &mut mesh.triangle_styles);
|
||||
}
|
||||
}
|
||||
(mesh.normals, mesh.triangle_normals) = stored_normals(&normal_coords, &corners.normals, mesh.triangles.len());
|
||||
(mesh.uvs, mesh.triangle_uvs) = stored_uvs(&texture, corners.textures);
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
/// `TESS_Face` (174) after its type code [WD 7.8.6].
|
||||
fn face(&mut self) -> Result<Face, PrcError> {
|
||||
let styles = self.uints("face line attributes")?;
|
||||
let _start_of_wire = self.r.unsigned_integer()?;
|
||||
let _sizes_wire = self.uints("face wire sizes")?;
|
||||
let flags = self.r.unsigned_integer()?;
|
||||
let start = self.r.unsigned_integer()? as usize;
|
||||
let data = self.uints("triangulated data")?;
|
||||
let textures = self.r.unsigned_integer()? as usize;
|
||||
let mut face = Face { flags, start, data, textures, styles, behaviour: 0 };
|
||||
if self.r.bit()? {
|
||||
let n = face.point_count()?;
|
||||
self.vertex_colors(n, false)?;
|
||||
}
|
||||
if !face.styles.is_empty() {
|
||||
let bits = self.r.unsigned_integer()?;
|
||||
face.behaviour = u16::try_from(bits).map_err(|_| malformed(format!("face behaviour {bits} is wider than 16 bits")))?;
|
||||
}
|
||||
self.schema.skip_added_fields(TESS_FACE, &mut self.r)?;
|
||||
Ok(face)
|
||||
}
|
||||
|
||||
/// `TESS_3D_Wire` (175) after its type code [WD 7.8.7].
|
||||
fn wire(&mut self) -> Result<Vec<Vec<[f64; 3]>>, PrcError> {
|
||||
let pts = points(&self.base_tess_data()?)?;
|
||||
let words = self.uints("wire indices")?;
|
||||
let mut wires: Vec<Vec<[f64; 3]>> = Vec::new();
|
||||
let mut colours = 0usize;
|
||||
if words.is_empty() {
|
||||
colours = pts.len();
|
||||
if !pts.is_empty() {
|
||||
wires.push(pts.clone());
|
||||
}
|
||||
}
|
||||
let mut it = words.iter().copied();
|
||||
while let Some(header) = it.next() {
|
||||
let n = (header & WIRE_COUNT_MASK) as usize;
|
||||
let mut wire = Vec::with_capacity(n.min(words.len()));
|
||||
for _ in 0..n {
|
||||
let idx = it.next().ok_or_else(|| malformed("wire runs past its indices".into()))?;
|
||||
wire.push(point_at(&pts, idx)?);
|
||||
}
|
||||
colours = colours.saturating_add(n);
|
||||
if header & WIRE_IS_CLOSING != 0 {
|
||||
colours = colours.saturating_add(1);
|
||||
if let Some(&first) = wire.first() {
|
||||
wire.push(first);
|
||||
}
|
||||
}
|
||||
match wires.last_mut() {
|
||||
Some(prev) if header & WIRE_IS_CONTINUOUS != 0 => prev.extend(wire),
|
||||
_ => wires.push(wire),
|
||||
}
|
||||
}
|
||||
if self.r.bit()? {
|
||||
self.vertex_colors(colours, true)?;
|
||||
}
|
||||
self.schema.skip_added_fields(TESS_3D_WIRE, &mut self.r)?;
|
||||
Ok(wires)
|
||||
}
|
||||
|
||||
/// `TESS_Markup` (176) after its type code [WD 7.8.8], read past.
|
||||
fn markup(&mut self) -> Result<(), PrcError> {
|
||||
self.base_tess_data()?;
|
||||
self.uints("markup codes")?;
|
||||
let n = self.count(1, "markup strings")?;
|
||||
for _ in 0..n {
|
||||
self.r.string()?;
|
||||
}
|
||||
self.r.string()?;
|
||||
self.r.character()?;
|
||||
self.schema.skip_added_fields(TESS_MARKUP, &mut self.r)
|
||||
}
|
||||
|
||||
/// `TESS_3D_Compressed` (173) after its type code [WD 7.8.9.7], read past; returns its
|
||||
/// triangle count. prc-rs: the field order and version gates follow its schema.
|
||||
///
|
||||
/// Sizes the WD leaves unstated: the face count is the largest face index + 1,
|
||||
/// `normal_is_reversed` is one bit per triangle, and the point-reference array's
|
||||
/// compressed flag is implicit (more than three references).
|
||||
fn tess_3d_compressed(&mut self) -> Result<usize, PrcError> {
|
||||
let r = &mut self.r;
|
||||
r.bit()?; // is_calculated
|
||||
r.bit()?; // has_faces
|
||||
r.double()?; // tolerance
|
||||
if self.version >= ORIGIN_FROM {
|
||||
for _ in 0..3 {
|
||||
r.float_as_bytes()?;
|
||||
}
|
||||
}
|
||||
arrays::compressed_integer_array(r)?; // point_array
|
||||
let edge_status = arrays::character_array(r, 2, None, false)?;
|
||||
let face_of = arrays::compressed_indice_array(r, None)?;
|
||||
let t = face_of.len();
|
||||
if edge_status.len() != t && edge_status.len() != t.saturating_mul(3) {
|
||||
return Err(malformed(format!("{} edge statuses for {t} triangles", edge_status.len())));
|
||||
}
|
||||
let faces = face_of.iter().max().map_or(0, |&m| (m as usize).saturating_add(1));
|
||||
let n = r.unsigned_integer()? as usize;
|
||||
let references = arrays::bool_array(r, n)?.iter().filter(|&&b| b).count();
|
||||
let refs = arrays::compressed_indice_array(r, Some(references > 3))?;
|
||||
if refs.len() != references {
|
||||
return Err(malformed(format!("{} point references for {references} flagged points", refs.len())));
|
||||
}
|
||||
self.compressed_normals(t, faces)?;
|
||||
let r = &mut self.r;
|
||||
if r.bit()? {
|
||||
arrays::bool_array(r, faces)?; // is_point_color_on_face
|
||||
arrays::character_array(r, 8, None, false)?; // point_color_array
|
||||
}
|
||||
if r.bit()? {
|
||||
arrays::bool_array(r, faces)?; // is_multiple_line_attribute_on_face
|
||||
}
|
||||
arrays::short_array(r, 16)?; // line_attributes
|
||||
if !r.bit()? {
|
||||
self.compressed_texture_parameter()?;
|
||||
if !self.r.bit()? {
|
||||
arrays::bool_array(&mut self.r, faces)?; // face_has_texture
|
||||
}
|
||||
}
|
||||
if self.r.bit()? {
|
||||
arrays::character_array(&mut self.r, 8, None, false)?; // behaviours
|
||||
}
|
||||
self.schema.skip_added_fields(TESS_3D_COMPRESSED, &mut self.r)?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// A compressed tessellation's normals, from `must_recalculate_normals` on, read past.
|
||||
fn compressed_normals(&mut self, triangles: usize, faces: usize) -> Result<(), PrcError> {
|
||||
let r = &mut self.r;
|
||||
if r.bit()? {
|
||||
arrays::bool_array(r, triangles)?; // normal_is_reversed
|
||||
r.double()?; // crease_angle
|
||||
r.character()?; // normal_recalculation_flags
|
||||
return Ok(());
|
||||
}
|
||||
let angle_bits = u32::from(r.character()?);
|
||||
if angle_bits > MAX_ANGLE_BITS {
|
||||
return Err(malformed(format!("{angle_bits}-bit normal angles")));
|
||||
}
|
||||
let n = r.unsigned_integer()? as usize;
|
||||
arrays::bool_array(r, n)?; // is_binary_normal
|
||||
arrays::short_array(r, angle_bits)?; // normal angles
|
||||
arrays::bool_array(r, faces)?; // is_face_planar
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `CompressedTextureParameter` [WD 7.8.9.8-7.8.9.9; pdf-issues #729, #749], read past.
|
||||
fn compressed_texture_parameter(&mut self) -> Result<(), PrcError> {
|
||||
let words = self.count(32, "texture data")?;
|
||||
// count() bounded words * 32 by the remaining bits.
|
||||
self.r.skip_bits(words.saturating_mul(32))?;
|
||||
self.r.unsigned_integer()?; // last_integer_used_bit_number
|
||||
let n = self.count(5, "texture references")?;
|
||||
for _ in 0..n {
|
||||
self.r.nbits_then_unsigned()?;
|
||||
}
|
||||
self.r.double()?; // tolerance
|
||||
let n = self.count(32, "texture parameters")?;
|
||||
for _ in 0..n {
|
||||
self.r.float_as_bytes()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The normal array and each triangle's corner normals, or both empty when any slot fails to
|
||||
/// name a normal: a mesh shades from its stored normals wholly or not at all.
|
||||
fn stored_normals(coords: &[f64], slots: &[[u32; 3]], triangles: usize) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
|
||||
// Like a slot naming no normal, a partial last normal drops the stored normals.
|
||||
let Ok(normals) = points(coords) else {
|
||||
return (Vec::new(), Vec::new());
|
||||
};
|
||||
let named = |k: u32| k.is_multiple_of(3) && (k as usize / 3) < normals.len();
|
||||
if normals.is_empty() || slots.len() != triangles || !slots.iter().flatten().all(|&k| named(k)) {
|
||||
return (Vec::new(), Vec::new());
|
||||
}
|
||||
let corners = slots.iter().map(|t| t.map(|k| k / 3)).collect();
|
||||
(normals, corners)
|
||||
}
|
||||
|
||||
/// Per texture-coordinate set, each triangle's corner pair indices.
|
||||
type UvSets = Vec<Vec<Option<[u32; 3]>>>;
|
||||
|
||||
/// The texture coordinates as (u, v) pairs, and per set the corner slots turned into pair
|
||||
/// indices (a slot is a `Double` offset, so even); a triangle with a slot naming no pair is
|
||||
/// `None`.
|
||||
fn stored_uvs(coords: &[f64], sets: UvSets) -> (Vec<[f64; 2]>, UvSets) {
|
||||
let uvs: Vec<[f64; 2]> = coords.as_chunks::<2>().0.to_vec();
|
||||
if uvs.is_empty() {
|
||||
return (Vec::new(), Vec::new());
|
||||
}
|
||||
let pair = |k: u32| (k.is_multiple_of(2) && (k as usize / 2) < uvs.len()).then_some(k / 2);
|
||||
let sets =
|
||||
sets.into_iter().map(|set| set.into_iter().map(|tri| tri.and_then(|[a, b, c]| Some([pair(a)?, pair(b)?, pair(c)?]))).collect()).collect();
|
||||
(uvs, sets)
|
||||
}
|
||||
|
||||
/// `coords` as points; an array that does not divide into whole points is malformed.
|
||||
fn points(coords: &[f64]) -> Result<Vec<[f64; 3]>, PrcError> {
|
||||
let (whole, rest) = coords.as_chunks::<3>();
|
||||
if !rest.is_empty() {
|
||||
return Err(malformed(format!("{} coordinates are not whole points", coords.len())));
|
||||
}
|
||||
Ok(whole.to_vec())
|
||||
}
|
||||
|
||||
/// The point a `Double`-offset index names [WD 7.8.5.2: multiples of 3].
|
||||
fn point_at(pts: &[[f64; 3]], idx: u32) -> Result<[f64; 3], PrcError> {
|
||||
if !idx.is_multiple_of(3) {
|
||||
return Err(malformed(format!("point index {idx} is not a multiple of 3")));
|
||||
}
|
||||
pts.get(idx as usize / 3).copied().ok_or_else(|| malformed(format!("point index {idx} past the coordinates")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
@@ -0,0 +1,738 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use super::triangulate::MAX_TEXTURE_SETS;
|
||||
use super::*;
|
||||
use crate::Schema;
|
||||
use crate::testw::W;
|
||||
|
||||
const SQUARE: [f64; 12] = [0., 0., 0., 1., 0., 0., 1., 1., 0., 0., 1., 0.];
|
||||
const ATTRIBUTE: u32 = 201;
|
||||
const NORMAL_SINGLE: u32 = 0x4000_0000;
|
||||
|
||||
/// A 305 section holding `body` (already-written entities, `n` of them).
|
||||
fn section(n: u32, body: &W) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(305).uint(0).bit(true).uint(n).append(body).uint(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn uints(w: &mut W, v: &[u32]) {
|
||||
w.uint(u32::try_from(v.len()).unwrap());
|
||||
for &x in v {
|
||||
w.uint(x);
|
||||
}
|
||||
}
|
||||
|
||||
fn square(w: &mut W, entity: u32) {
|
||||
coordinates(w, entity, &SQUARE);
|
||||
}
|
||||
|
||||
fn coordinates(w: &mut W, entity: u32, coords: &[f64]) {
|
||||
w.uint(entity).bit(false).uint(u32::try_from(coords.len()).unwrap());
|
||||
for &c in coords {
|
||||
w.double(c);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Mesh<'a> {
|
||||
recalc: bool,
|
||||
indices: &'a [u32],
|
||||
faces: &'a [(u32, u32, &'a [u32])],
|
||||
textures: u32,
|
||||
uvs: &'a [f64],
|
||||
/// The coordinates; [`SQUARE`] when `None`.
|
||||
coords: Option<&'a [f64]>,
|
||||
}
|
||||
|
||||
/// A TESS_3D over [`SQUARE`] at authoring version `v`.
|
||||
fn tess_3d(w: &mut W, v: u32, m: &Mesh<'_>) {
|
||||
coordinates(w, 172, m.coords.unwrap_or(&SQUARE));
|
||||
w.bit(true);
|
||||
if v >= 7039 {
|
||||
w.bit(false);
|
||||
}
|
||||
if v >= 7047 {
|
||||
w.bit(m.recalc);
|
||||
if m.recalc {
|
||||
w.put(0, 8).double(0.5);
|
||||
}
|
||||
}
|
||||
if m.recalc {
|
||||
w.uint(0);
|
||||
} else {
|
||||
// Two normals: slot 0 is +z, slot 3 is -z.
|
||||
w.uint(6);
|
||||
for c in [0., 0., 1., 0., 0., -1.] {
|
||||
w.double(c);
|
||||
}
|
||||
}
|
||||
w.uint(0);
|
||||
uints(w, m.indices);
|
||||
w.uint(u32::try_from(m.faces.len()).unwrap());
|
||||
for &(flags, start, data) in m.faces {
|
||||
w.uint(174).uint(0).uint(0).uint(0).uint(flags).uint(start);
|
||||
uints(w, data);
|
||||
w.uint(m.textures).bit(false);
|
||||
}
|
||||
w.uint(u32::try_from(m.uvs.len()).unwrap());
|
||||
for &c in m.uvs {
|
||||
w.double(c);
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(w: &W, schema: &Schema, version: u32) -> Result<Vec<Tessellation>, PrcError> {
|
||||
let bytes = w.bytes();
|
||||
Ctx::new(BitReader::new(&bytes), schema, version).file_structure_tessellation()
|
||||
}
|
||||
|
||||
fn mesh(t: &Tessellation) -> &TriangleMesh {
|
||||
match t {
|
||||
Tessellation::Mesh(m) => m,
|
||||
other => panic!("not a mesh: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn one(indices: &[u32], faces: &[(u32, u32, &[u32])], textures: u32) -> TriangleMesh {
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices, faces, textures, ..Mesh::default() });
|
||||
mesh(&decode(§ion(1, &b), &Schema::default(), 8137).unwrap()[0]).clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triangle_and_fan_with_normals() {
|
||||
// One triangle (n,p x3), then a fan of 4 vertices (n,p each).
|
||||
let m = one(&[3, 0, 0, 3, 3, 6, 0, 0, 3, 3, 0, 6, 3, 9], &[(0x2 | 0x4, 0, &[1, 1, 4])], 0);
|
||||
assert_eq!(m.positions.len(), 4);
|
||||
assert_eq!(m.positions[2], [1., 1., 0.]);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [0, 1, 2], [0, 2, 3]]);
|
||||
assert_eq!(m.faces, vec![0..3]);
|
||||
assert!(!m.normals_recalculated);
|
||||
assert_eq!(m.normals, [[0., 0., 1.], [0., 0., -1.]]);
|
||||
// Each corner keeps the normal stored beside its point.
|
||||
assert_eq!(m.triangle_normals, [[1, 0, 1], [0, 1, 0], [0, 0, 1]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_normals_follow_the_alternating_winding() {
|
||||
let m = one(&[3, 0, 0, 3, 3, 6, 0, 9], &[(0x8, 0, &[1, 4])], 0);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3]]);
|
||||
assert_eq!(m.triangle_normals, [[1, 0, 1], [1, 0, 0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_normal_index_past_the_array_drops_every_normal_but_no_triangle() {
|
||||
let m = one(&[0, 0, 6, 3, 0, 6], &[(0x2, 0, &[1])], 0);
|
||||
assert_eq!(m.triangles, [[0, 1, 2]]);
|
||||
assert!(m.normals.is_empty());
|
||||
assert!(m.triangle_normals.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recalculated_normals_drop_the_normal_slots_and_strips_alternate() {
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { recalc: true, indices: &[0, 3, 6, 9], faces: &[(0x8, 0, &[1, 4])], ..Mesh::default() });
|
||||
let t = decode(§ion(1, &b), &Schema::default(), 8137).unwrap();
|
||||
let m = mesh(&t[0]);
|
||||
assert!(m.normals_recalculated);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [2, 1, 3]]);
|
||||
assert!(m.normals.is_empty() && m.triangle_normals.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_gates_the_loop_and_recalculation_flags() {
|
||||
for v in [7038, 7039, 7046, 7047, 8137] {
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, v, &Mesh { indices: &[0, 0, 0, 3, 0, 6], faces: &[(0x2, 0, &[1])], ..Mesh::default() });
|
||||
// A trailing wire: a misread flag desyncs it.
|
||||
wire(&mut b, false);
|
||||
let t = decode(§ion(2, &b), &Schema::default(), v).unwrap();
|
||||
assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2]], "version {v}");
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)), "version {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_normal_blocks_and_textures() {
|
||||
// 0x20: n,p,p,p. 0x40 with NORMAL_Single: n then p per vertex.
|
||||
// 0x2000 with one texture set: n,t,p,t,p,t,p.
|
||||
let idx = [
|
||||
0, 0, 3, 6, // 0x20
|
||||
3, 0, 6, 9, // 0x40 single, 3 vertices
|
||||
0, 0, 9, 2, 0, 4, 3, // 0x2000
|
||||
];
|
||||
let m = one(&idx, &[(0x20 | 0x40 | 0x2000, 0, &[1, 1, 3 | NORMAL_SINGLE, 1])], 1);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3], [3, 0, 1]]);
|
||||
// A one-normal entity gives every corner its single normal.
|
||||
assert_eq!(m.triangle_normals, [[0, 0, 0], [1, 1, 1], [0, 0, 0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_normal_fan_without_single_keeps_a_normal_per_vertex() {
|
||||
let m = one(&[0, 0, 0, 3, 0, 6, 0, 9], &[(0x40, 0, &[1, 4])], 0);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn textured_fan_and_strip_carry_texture_slots_per_vertex() {
|
||||
// 0x400 then 0x800, two texture sets: n,t,t,p per vertex.
|
||||
let mut idx = Vec::new();
|
||||
for p in [0, 3, 6, 0, 3, 6, 9] {
|
||||
idx.extend([0, 0, 2, p]);
|
||||
}
|
||||
let m = one(&idx, &[(0x400 | 0x800, 0, &[1, 3, 1, 4])], 2);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [0, 1, 2], [2, 1, 3]]);
|
||||
}
|
||||
|
||||
/// The unit square as two textured triangles (one set; per vertex a normal, a texture slot,
|
||||
/// a point), its corners at the picture's corners.
|
||||
fn textured_square() -> W {
|
||||
let mut body = W::default();
|
||||
let indices = [0, 0, 0, 0, 2, 3, 0, 4, 6, 0, 0, 0, 0, 4, 6, 0, 6, 9];
|
||||
tess_3d(
|
||||
&mut body,
|
||||
8137,
|
||||
&Mesh { indices: &indices, faces: &[(0x200, 0, &[2])], textures: 1, uvs: &[0., 0., 1., 0., 1., 1., 0., 1.], ..Mesh::default() },
|
||||
);
|
||||
section(1, &body)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_textured_mesh_keeps_each_corners_coordinates() {
|
||||
let t = decode(&textured_square(), &Schema::default(), 8137).unwrap();
|
||||
let m = mesh(&t[0]);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3]]);
|
||||
assert_eq!(m.uvs, [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
|
||||
assert_eq!(m.triangle_uvs, [vec![Some([0, 1, 2]), Some([0, 2, 3])]]);
|
||||
// An odd slot names no pair: that triangle has no coordinates.
|
||||
let mut b = W::default();
|
||||
tess_3d(
|
||||
&mut b,
|
||||
8137,
|
||||
&Mesh { indices: &[0, 1, 0, 0, 2, 3, 0, 4, 6], faces: &[(0x200, 0, &[1])], textures: 1, uvs: &[0.0; 6], ..Mesh::default() },
|
||||
);
|
||||
let t = decode(§ion(1, &b), &Schema::default(), 8137).unwrap();
|
||||
let m = mesh(&t[0]);
|
||||
assert_eq!(m.triangles, [[0, 1, 2]]);
|
||||
assert_eq!(m.triangle_uvs, [vec![None]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_texture_sets_are_refused_before_anything_is_allocated() {
|
||||
// An empty fan on a face claiming 2^32 - 1 texture sets: no slot bounds the count.
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &[0], faces: &[(0x400, 0, &[1, 0])], textures: u32::MAX, ..Mesh::default() });
|
||||
assert!(
|
||||
matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(m)) if m == format!("{} texture-coordinate sets on a face", u32::MAX))
|
||||
);
|
||||
let sets = u32::try_from(MAX_TEXTURE_SETS).unwrap();
|
||||
let mut ok = W::default();
|
||||
tess_3d(&mut ok, 8137, &Mesh { indices: &[0], faces: &[(0x400, 0, &[1, 0])], textures: sets, ..Mesh::default() });
|
||||
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);
|
||||
assert_eq!(m.triangles, [[0, 1, 2], [0, 2, 3]]);
|
||||
assert_eq!(m.faces, [0..1, 1..2]);
|
||||
}
|
||||
|
||||
/// A wire over [`SQUARE`]: an open two-point wire continued by a closing two-point wire,
|
||||
/// then optionally per-segment RGB colours.
|
||||
fn wire(w: &mut W, colours: bool) {
|
||||
square(w, 175);
|
||||
uints(w, &[2, 0, 3, WIRE_IS_CONTINUOUS | WIRE_IS_CLOSING | 2, 6, 9]);
|
||||
w.bit(colours);
|
||||
if colours {
|
||||
// 5 points (4 + the implicit closing one) -> 2 segment colours.
|
||||
w.bit(false).bit(true).bit(false).put(0xff0000, 24);
|
||||
w.bit(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wires_close_and_continue_and_colours_are_counted() {
|
||||
for colours in [false, true] {
|
||||
let mut b = W::default();
|
||||
wire(&mut b, colours);
|
||||
// A second entity: a wrong colour count would desync it.
|
||||
wire(&mut b, false);
|
||||
let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap();
|
||||
for e in &t {
|
||||
let Tessellation::Wire(w) = e else { panic!("not a wire") };
|
||||
assert_eq!(w, &[vec![[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.], [1., 1., 0.]]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closing_wire_colours_its_implicit_point() {
|
||||
// Per-point RGB: 4 indexed points + 1 implicit = 5 colours.
|
||||
let mut b = W::default();
|
||||
square(&mut b, 175);
|
||||
uints(&mut b, &[2, 0, 3, WIRE_IS_CONTINUOUS | WIRE_IS_CLOSING | 2, 6, 9]);
|
||||
b.bit(true).bit(false).bit(false).bit(false).put(1, 24);
|
||||
for _ in 1..5 {
|
||||
b.bit(true);
|
||||
}
|
||||
wire(&mut b, false);
|
||||
let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap();
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)));
|
||||
assert_eq!(t[0], t[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wire_index_must_name_a_point() {
|
||||
let mut b = W::default();
|
||||
square(&mut b, 175);
|
||||
uints(&mut b, &[2, 0, 12]);
|
||||
b.bit(false);
|
||||
assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wire_without_indices_is_the_coordinate_polyline() {
|
||||
let mut b = W::default();
|
||||
square(&mut b, 175);
|
||||
b.uint(0).bit(true);
|
||||
// 4 implicit points -> 4 RGB colours, all "same" after the first.
|
||||
b.bit(false).bit(false).bit(false).put(1, 24);
|
||||
for _ in 1..4 {
|
||||
b.bit(true);
|
||||
}
|
||||
wire(&mut b, false);
|
||||
let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap();
|
||||
let Tessellation::Wire(w) = &t[0] else { panic!("not a wire") };
|
||||
assert_eq!(w.len(), 1);
|
||||
assert_eq!(w[0].len(), 4);
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)));
|
||||
}
|
||||
|
||||
/// Reused colours cost one bit each, so a long run of them may end the section.
|
||||
#[test]
|
||||
fn reused_vertex_colours_may_be_the_last_data() {
|
||||
let n = 40;
|
||||
let mut b = W::default();
|
||||
b.uint(175).bit(false).uint(3 * n);
|
||||
for i in 0..3 * n {
|
||||
b.double(f64::from(i));
|
||||
}
|
||||
// No indices: the polyline of all `n` points, one RGB colour then `n - 1` reuses.
|
||||
b.uint(0).bit(true).bit(false).bit(false).bit(false).put(1, 24);
|
||||
for _ in 1..n {
|
||||
b.bit(true);
|
||||
}
|
||||
let t = decode(§ion(1, &b), &Schema::default(), 8137).unwrap();
|
||||
let Tessellation::Wire(w) = &t[0] else { panic!("not a wire") };
|
||||
assert_eq!(w[0].len(), n as usize);
|
||||
}
|
||||
|
||||
/// A square as one triangle plus one 4-vertex fan (two entities, three triangles), the face
|
||||
/// carrying `attrs` and, when it has any, behaviour 0x30; then `colours` per-point colours
|
||||
/// and a trailing wire.
|
||||
fn styled_face(attrs: &[u32], colours: usize) -> Vec<Tessellation> {
|
||||
decode(&styled_face_section(attrs, colours, 0x30), &Schema::default(), 8137).unwrap()
|
||||
}
|
||||
|
||||
/// [`styled_face`]'s section, the face's behaviour `behaviour`.
|
||||
fn styled_face_section(attrs: &[u32], colours: usize, behaviour: u32) -> W {
|
||||
let mut b = W::default();
|
||||
square(&mut b, 172);
|
||||
b.bit(true).bit(false).bit(false).uint(0).uint(0);
|
||||
uints(&mut b, &[0, 0, 0, 3, 0, 6, 0, 0, 0, 3, 0, 6, 0, 9]);
|
||||
b.uint(1).uint(174);
|
||||
uints(&mut b, attrs);
|
||||
b.uint(0).uint(0).uint(0x2 | 0x4).uint(0);
|
||||
uints(&mut b, &[1, 1, 4]);
|
||||
b.uint(0).bit(colours > 0);
|
||||
if colours > 0 {
|
||||
b.bit(false).bit(false).put(0x10_2030, 24);
|
||||
for _ in 1..colours {
|
||||
b.bit(true);
|
||||
}
|
||||
}
|
||||
if !attrs.is_empty() {
|
||||
b.uint(behaviour);
|
||||
}
|
||||
b.uint(0);
|
||||
wire(&mut b, false);
|
||||
section(2, &b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_vertex_colours_count_points_not_data() {
|
||||
// A triangle plus a fan of 4 vertices = 7 points -> 7 colours.
|
||||
let t = styled_face(&[], 7);
|
||||
assert_eq!(mesh(&t[0]).triangles.len(), 3);
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)));
|
||||
}
|
||||
|
||||
fn styles(attrs: &[u32]) -> Vec<(u32, u16)> {
|
||||
let t = styled_face(attrs, 0);
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)));
|
||||
mesh(&t[0]).triangle_styles.iter().map(|g| (g.style, g.behaviour)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_line_attributes_style_its_triangles() {
|
||||
assert!(styles(&[]).is_empty());
|
||||
assert_eq!(styles(&[5]), [(5, 0x30); 3]);
|
||||
// One per entity: the triangle, then both triangles of the fan.
|
||||
assert_eq!(styles(&[4, 7]), [(4, 0x30), (7, 0x30), (7, 0x30)]);
|
||||
// An entity styled 0 inherits, as do entities past the list.
|
||||
assert_eq!(styles(&[4, 0, 9]), [(4, 0x30), (0, 0x30), (0, 0x30)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markup_is_read_past() {
|
||||
let mut b = W::default();
|
||||
b.uint(176).bit(false).uint(3);
|
||||
for c in [1., 2., 3.] {
|
||||
b.double(c);
|
||||
}
|
||||
uints(&mut b, &[6, 0]);
|
||||
b.uint(1).string(Some("R1")).string(Some("label")).put(7, 8);
|
||||
wire(&mut b, false);
|
||||
let t = decode(§ion(2, &b), &Schema::default(), 8137).unwrap();
|
||||
assert_eq!(t[0], Tessellation::Markup);
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_fields_are_skipped_after_each_entity() {
|
||||
// A newer producer added one UnsignedInteger to TESS_3D and one to TESS_Face.
|
||||
let mut s = W::default();
|
||||
s.uint(2).uint(172).uint(1).uint(3).uint(174).uint(1).uint(3);
|
||||
let sb = s.bytes();
|
||||
let schema = Schema::read(&mut BitReader::new(&sb)).unwrap();
|
||||
|
||||
let mut b = W::default();
|
||||
square(&mut b, 172);
|
||||
b.bit(true).bit(false).bit(false).uint(0).uint(0);
|
||||
uints(&mut b, &[0, 0, 0, 3, 0, 6]);
|
||||
b.uint(1).uint(174).uint(0).uint(0).uint(0).uint(0x2).uint(0);
|
||||
uints(&mut b, &[1]);
|
||||
b.uint(0).bit(false).uint(99); // the face's added field
|
||||
b.uint(0).uint(77); // texture count, then TESS_3D's added field
|
||||
wire(&mut b, false);
|
||||
let t = decode(§ion(2, &b), &schema, 8137).unwrap();
|
||||
assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2]]);
|
||||
assert!(matches!(t[1], Tessellation::Wire(_)));
|
||||
// Without the schema the same bytes do not decode to the same thing.
|
||||
assert_ne!(decode(§ion(2, &b), &Schema::default(), 8137).ok(), Some(t));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_names_and_user_data_are_read_past() {
|
||||
let mut w = W::default();
|
||||
w.uint(305).uint(1).uint(ATTRIBUTE);
|
||||
w.bit(true).uint(2); // predefined title
|
||||
w.uint(2);
|
||||
w.bit(false).string(Some("k")).uint(4).string(Some("v"));
|
||||
w.bit(true).uint(1).uint(5).int(-1).uint(9);
|
||||
w.uint(42); // the attribute's schema-added field
|
||||
w.bit(false).string(Some("tess")).uint(1);
|
||||
wire(&mut w, false);
|
||||
w.uint(5).put(0b10110, 5);
|
||||
let mut sw = W::default();
|
||||
sw.uint(1).uint(ATTRIBUTE).uint(1).uint(3);
|
||||
let sb = sw.bytes();
|
||||
let schema = Schema::read(&mut BitReader::new(&sb)).unwrap();
|
||||
let bytes = w.bytes();
|
||||
let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, 8137);
|
||||
let t = ctx.file_structure_tessellation().unwrap();
|
||||
assert!(matches!(t[..], [Tessellation::Wire(_)]));
|
||||
assert_eq!(ctx.name.as_deref(), Some("tess"));
|
||||
assert_eq!(ctx.r.position(), w.len());
|
||||
}
|
||||
|
||||
/// A one-triangle TESS_3D_Compressed. `full` takes every optional branch: stored normals,
|
||||
/// colours, line attributes, texture, behaviours. Otherwise `three_t` picks the 3T edge form.
|
||||
fn compressed(w: &mut W, v: u32, full: bool, three_t: bool) {
|
||||
compressed_with_angles(w, v, full, three_t, 10);
|
||||
}
|
||||
|
||||
/// [`compressed`] whose `full` form stores `angle_bits`-bit normal angles.
|
||||
fn compressed_with_angles(w: &mut W, v: u32, full: bool, three_t: bool, angle_bits: u8) {
|
||||
w.uint(173).bit(false).bit(true).double(0.001);
|
||||
if v >= 7031 {
|
||||
w.put(0, 32).put(0, 32).put(0, 32);
|
||||
}
|
||||
// point_array: nine 2-bit values.
|
||||
w.bit(false).uint(9);
|
||||
for _ in 0..9 {
|
||||
w.put(2, 8);
|
||||
}
|
||||
for _ in 0..9 {
|
||||
w.bit(false).bit(true);
|
||||
}
|
||||
// edge_status_array: the 3T form (Huffman) or the T form.
|
||||
if full {
|
||||
w.bit(true).huffman(2, 2, &[(0, 0b10, 2)], &[0, 0, 0]);
|
||||
} else if three_t {
|
||||
w.bit(false).uint(3).put(0, 8).put(0, 8).put(0, 8);
|
||||
} else {
|
||||
w.bit(false).uint(1).put(0, 8);
|
||||
}
|
||||
// triangle_face_array [0]. `full`: four references, so the flagless reference array is
|
||||
// Huffman; else none.
|
||||
w.bit(false).uint(1).put(1, 8).bit(false);
|
||||
if full {
|
||||
w.uint(4).put(0b1111, 4);
|
||||
w.huffman(6, 2, &[(1, 0b10, 2), (0, 0b11, 2)], &[1, 0, 0, 0]);
|
||||
w.put(0, 4);
|
||||
} else {
|
||||
w.uint(3).put(0, 3).uint(0);
|
||||
}
|
||||
if full {
|
||||
w.bit(false).put(u64::from(angle_bits), 8).uint(3).put(0b101, 3);
|
||||
w.bit(true).huffman(10, 2, &[(5, 0b10, 2)], &[5, 5]);
|
||||
w.bit(true); // is_face_planar
|
||||
w.bit(true).bit(true).bit(false).uint(5);
|
||||
for c in [1, 255, 0, 0, 128] {
|
||||
w.put(c, 8);
|
||||
}
|
||||
w.bit(true).bit(false);
|
||||
} else {
|
||||
w.bit(true).bit(false).double(0.5).put(0, 8);
|
||||
w.bit(false).bit(false);
|
||||
}
|
||||
w.bit(false).uint(1).put(1, 8).put(0, 8); // line_attribute_array
|
||||
if full {
|
||||
w.bit(false).uint(1).put(0, 32).uint(32);
|
||||
w.uint(1).put(1, 5).bit(true).double(0.5).uint(2);
|
||||
w.put(0, 32).put(0, 32).bit(false).bit(true);
|
||||
w.bit(true).bit(false).uint(1).put(1, 8);
|
||||
} else {
|
||||
w.bit(true).bit(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_compressed_mesh_is_read_to_its_end() {
|
||||
for (full, three_t) in [(false, false), (false, true), (true, true)] {
|
||||
for v in [7030, 7031, 8137] {
|
||||
let mut b = W::default();
|
||||
compressed(&mut b, v, full, three_t);
|
||||
let indices = [0, 0, 0, 3, 0, 6, 0, 0, 0, 3, 0, 6, 0, 9];
|
||||
tess_3d(&mut b, v, &Mesh { indices: &indices, faces: &[(0x2 | 0x4, 0, &[1, 1, 4])], ..Mesh::default() });
|
||||
let w = section(2, &b);
|
||||
let bytes = w.bytes();
|
||||
let schema = Schema::default();
|
||||
let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, v);
|
||||
let t = ctx.file_structure_tessellation().unwrap();
|
||||
assert!(matches!(t[..], [Tessellation::Compressed { triangles: 1, .. }, Tessellation::Mesh(_)]), "full={full} v={v}: {t:?}");
|
||||
assert_eq!(ctx.r.position(), w.len(), "full={full} v={v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refusals_and_damage_are_errors() {
|
||||
// Edge statuses neither T nor 3T long.
|
||||
let mut c = W::default();
|
||||
c.uint(173).bit(false).bit(true).double(0.001);
|
||||
c.put(0, 32).put(0, 32).put(0, 32);
|
||||
c.bit(false).uint(0).bit(false).uint(2).put(0, 16);
|
||||
c.bit(false).uint(1).put(1, 8).bit(false);
|
||||
assert!(matches!(decode(§ion(1, &c), &Schema::default(), 8137), Err(PrcError::Malformed(_))));
|
||||
// Optimised vertex colours.
|
||||
let mut o = W::default();
|
||||
square(&mut o, 175);
|
||||
o.uint(0).bit(true).bit(false).bit(false).bit(true);
|
||||
assert!(matches!(decode(§ion(1, &o), &Schema::default(), 8137), Err(PrcError::Unsupported(_))));
|
||||
// A point index that is not a multiple of 3, and one past the end.
|
||||
for bad in [4, 12] {
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0, 0, bad, 0, 6], faces: &[(0x2, 0, &[1])], ..Mesh::default() });
|
||||
assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
// A fan whose count claims a billion points.
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0], faces: &[(0x4, 0, &[1, 0x3FFF_FFFF])], ..Mesh::default() });
|
||||
assert!(decode(§ion(1, &b), &Schema::default(), 8137).is_err());
|
||||
// A tessellation count far past the data.
|
||||
let mut w = W::default();
|
||||
w.uint(305).uint(0).bit(true).uint(u32::MAX);
|
||||
assert!(matches!(decode(&w, &Schema::default(), 8137), Err(PrcError::Truncated(_))));
|
||||
// A wrong entity type where the section's own belongs.
|
||||
let mut w = W::default();
|
||||
w.uint(306);
|
||||
assert!(matches!(decode(&w, &Schema::default(), 8137), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_truncation_of_a_section_is_an_error_not_a_panic() {
|
||||
let mut b = W::default();
|
||||
compressed(&mut b, 8137, true, true);
|
||||
wire(&mut b, true);
|
||||
let bytes = section(2, &b).bytes();
|
||||
for end in 0..bytes.len() {
|
||||
let cut = bytes.get(..end).unwrap();
|
||||
// The zero padding of the last byte may still decode; anything shorter must not.
|
||||
let _ = Ctx::new(BitReader::new(cut), &Schema::default(), 8137).file_structure_tessellation();
|
||||
}
|
||||
let cut = bytes.get(..bytes.len() / 2).unwrap();
|
||||
assert!(Ctx::new(BitReader::new(cut), &Schema::default(), 8137).file_structure_tessellation().is_err());
|
||||
}
|
||||
|
||||
/// A one-file-structure PRC stream whose tessellation section is `tess`, with an empty
|
||||
/// globals section schema.
|
||||
fn prc_file(tess: &[u8]) -> Vec<u8> {
|
||||
let mut globals = W::default();
|
||||
globals.uint(0);
|
||||
prc_file_with(&globals.bytes(), tess)
|
||||
}
|
||||
|
||||
/// As [`prc_file`] with the globals section `globals`.
|
||||
fn prc_file_with(globals: &[u8], tess: &[u8]) -> Vec<u8> {
|
||||
use std::io::Write as _;
|
||||
const HEADER_LEN: usize = 107;
|
||||
let zlib = |data: &[u8]| {
|
||||
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
e.write_all(data).unwrap();
|
||||
e.finish().unwrap()
|
||||
};
|
||||
let u32s = |out: &mut Vec<u8>, vs: &[u32]| vs.iter().for_each(|v| out.extend_from_slice(&v.to_le_bytes()));
|
||||
let sections: [&[u8]; 5] = [globals, b"", tess, b"", b""];
|
||||
let mut body = b"PRC".to_vec();
|
||||
u32s(&mut body, &[8137, 8137, 5, 6, 7, 8, 0, 0, 0, 0, 0]);
|
||||
let mut offs = vec![HEADER_LEN];
|
||||
for s in sections {
|
||||
offs.push(HEADER_LEN + body.len());
|
||||
body.extend(zlib(s));
|
||||
}
|
||||
let mf_start = HEADER_LEN + body.len();
|
||||
body.extend(zlib(b""));
|
||||
let mf_end = HEADER_LEN + body.len();
|
||||
let mut out = b"PRC".to_vec();
|
||||
u32s(&mut out, &[8137, 8137, 1, 2, 3, 4, 0, 0, 0, 0, 1]);
|
||||
u32s(&mut out, &[5, 6, 7, 8, 0, 6]);
|
||||
u32s(&mut out, &offs.iter().map(|&o| u32::try_from(o).unwrap()).collect::<Vec<_>>());
|
||||
u32s(&mut out, &[u32::try_from(mf_start).unwrap(), u32::try_from(mf_end).unwrap(), 0]);
|
||||
out.extend(body);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prc_file_yields_its_tessellations() {
|
||||
let f = crate::PrcFile::parse(&prc_file(&textured_square().bytes())).unwrap();
|
||||
let t = f.file_structures[0].tessellations().unwrap();
|
||||
assert_eq!(mesh(&t[0]).triangles, [[0, 1, 2], [0, 2, 3]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_angles_wider_than_16_bits_are_refused_by_name() {
|
||||
let mut b = W::default();
|
||||
compressed_with_angles(&mut b, 8137, true, true, 17);
|
||||
assert!(matches!(decode(§ion(1, &b), &Schema::default(), 8137), Err(PrcError::Malformed(m)) if m == "17-bit normal angles"));
|
||||
}
|
||||
|
||||
/// An entity's counted array is charged against the reader's element allowance.
|
||||
#[test]
|
||||
fn counted_entity_arrays_are_charged_against_the_allowance() {
|
||||
let mut w = W::default();
|
||||
w.uint(3).uint(1).uint(2).uint(3);
|
||||
let bytes = w.bytes();
|
||||
let schema = Schema::default();
|
||||
let read = |n| Ctx::new(BitReader::with_elements(&bytes, n), &schema, 8137).uints("indices");
|
||||
assert!(matches!(read(2), Err(PrcError::TooManyElements { .. })));
|
||||
assert_eq!(read(3).unwrap(), [1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_face_behaviour_wider_than_16_bits_is_malformed() {
|
||||
let r = decode(&styled_face_section(&[1], 0, 0x1_0030), &Schema::default(), 8137);
|
||||
assert!(matches!(r, Err(PrcError::Malformed(m)) if m == "face behaviour 65584 is wider than 16 bits"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coordinates_that_are_not_whole_points_are_malformed() {
|
||||
let mut partial = SQUARE.to_vec();
|
||||
partial.push(7.0);
|
||||
let mut b = W::default();
|
||||
let faces: &[(u32, u32, &[u32])] = &[(0x2, 0, &[1])];
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &[0, 0, 0, 3, 0, 6], faces, coords: Some(&partial), ..Mesh::default() });
|
||||
let r = decode(§ion(1, &b), &Schema::default(), 8137);
|
||||
assert!(matches!(r, Err(PrcError::Malformed(m)) if m == "13 coordinates are not whole points"));
|
||||
// A partial last normal drops the stored normals, as a slot naming none does.
|
||||
assert_eq!(stored_normals(&[0.0, 0.0, 1.0, 0.0], &[[0, 0, 0]], 1), (Vec::new(), Vec::new()));
|
||||
}
|
||||
|
||||
/// Elements a tessellation section spends from a full allowance.
|
||||
fn spent(section: &W) -> usize {
|
||||
let bytes = section.bytes();
|
||||
let schema = Schema::default();
|
||||
let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, 8137);
|
||||
ctx.file_structure_tessellation().unwrap();
|
||||
crate::MAX_DECODED_ELEMENTS - ctx.r.elements_left()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faces_reusing_one_index_range_are_charged_for_each_triangle_they_make() {
|
||||
const INDICES: [u32; 12] = [0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9];
|
||||
let square = |faces: &[(u32, u32, &[u32])]| {
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &INDICES, faces, ..Mesh::default() });
|
||||
spent(§ion(1, &b))
|
||||
};
|
||||
let one = square(&[(0x2, 0, &[2])]);
|
||||
let three = square(&[(0x2, 0, &[2]), (0x2, 0, &[2]), (0x2, 0, &[2])]);
|
||||
// Per extra face: its count entry, its one data element and its two triangles, each
|
||||
// 12 bytes of corners and 12 of normal slots.
|
||||
assert_eq!(three - one, 2 * (2 + 2 * 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reused_textured_faces_are_charged_for_every_corner_array() {
|
||||
// The textured square, then one untextured triangle's normal and point slots.
|
||||
const INDICES: [u32; 24] = [0, 0, 0, 0, 2, 3, 0, 4, 6, 0, 0, 0, 0, 4, 6, 0, 6, 9, 0, 0, 0, 3, 0, 6];
|
||||
let square = |faces: &[(u32, u32, &[u32])], sets: u32| {
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &INDICES, faces, textures: sets, uvs: &[0.0; 8], ..Mesh::default() });
|
||||
spent(§ion(1, &b))
|
||||
};
|
||||
let face: (u32, u32, &[u32]) = (0x200, 0, &[2]);
|
||||
let untextured: (u32, u32, &[u32]) = (0x2, 18, &[1]);
|
||||
let one = square(&[face], 1);
|
||||
let three = square(&[face, face, face], 1);
|
||||
// Per extra face: its count, its data element and two triangles of corners, normal slots
|
||||
// and one texture set, 16 bytes a corner triple.
|
||||
assert_eq!(three - one, 2 * (2 + 2 * 6));
|
||||
// One textured face gives every triangle of the mesh a texture triple.
|
||||
let mixed = square(&[face, untextured], 1);
|
||||
assert_eq!(mixed - one, 2 + 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_face_is_charged_before_it_is_triangulated() {
|
||||
// One face claiming a million triangles over a 12-entry index array.
|
||||
const INDICES: [u32; 12] = [0, 0, 0, 3, 0, 6, 0, 0, 0, 6, 0, 9];
|
||||
let mut b = W::default();
|
||||
tess_3d(&mut b, 8137, &Mesh { indices: &INDICES, faces: &[(0x2, 0, &[1_000_000])], ..Mesh::default() });
|
||||
let bytes = section(1, &b).bytes();
|
||||
let schema = Schema::default();
|
||||
let r = Ctx::new(BitReader::with_elements(&bytes, 1000), &schema, 8137).file_structure_tessellation();
|
||||
assert!(matches!(r, Err(PrcError::TooManyElements { .. })), "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_globals_schema_and_the_tessellations_share_one_allowance() {
|
||||
// A schema of one entry with three tokens: four elements.
|
||||
let mut globals = W::default();
|
||||
globals.uint(1).uint(999).uint(3).uint(0).uint(0).uint(0);
|
||||
let tess = textured_square();
|
||||
let f = crate::PrcFile::parse(&prc_file_with(&globals.bytes(), &tess.bytes())).unwrap();
|
||||
let need = spent(&tess) + 4;
|
||||
let within = |elements| f.file_structures[0].tessellations_within(crate::bits::Budget { elements, ..crate::bits::Budget::FULL });
|
||||
assert!(within(need).is_ok());
|
||||
assert!(matches!(within(need - 1), Err(PrcError::TooManyElements { .. })));
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
//! A `TESS_Face`'s triangulated data turned into triangles [WD 7.8.5.5, 7.8.6].
|
||||
|
||||
use super::TriangleStyle;
|
||||
use crate::PrcError;
|
||||
use crate::entity::malformed;
|
||||
|
||||
/// A fan/strip count's point-count bits; bit 30 is `NORMAL_Single` [WD 7.8.6.2].
|
||||
const COUNT_MASK: u32 = 0x3FFF_FFFF;
|
||||
const NORMAL_SINGLE: u32 = 0x4000_0000;
|
||||
|
||||
/// Texture-coordinate sets a face may carry. Each set costs a slot per point and a corner
|
||||
/// triple per triangle of the whole mesh, so the count is capped.
|
||||
pub(super) const MAX_TEXTURE_SETS: usize = 8;
|
||||
|
||||
pub(super) struct Face {
|
||||
pub(super) flags: u32,
|
||||
pub(super) start: usize,
|
||||
pub(super) data: Vec<u32>,
|
||||
pub(super) textures: usize,
|
||||
/// `line_attributes`, each a style index + 1: none = the owner's graphics, one = the
|
||||
/// whole face's, more = one per triangulation entity [WD 7.8.6].
|
||||
pub(super) styles: Vec<u32>,
|
||||
/// The behaviour bits read when `styles` is not empty.
|
||||
pub(super) behaviour: u16,
|
||||
}
|
||||
|
||||
/// A `used_entities_flag` block's shape [WD 7.8.5.5].
|
||||
#[derive(Clone, Copy)]
|
||||
enum Shape {
|
||||
Triangles,
|
||||
Fan,
|
||||
Strip,
|
||||
}
|
||||
|
||||
/// One `used_entities_flag` block.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Block {
|
||||
bit: u32,
|
||||
shape: Shape,
|
||||
/// One normal per entity rather than per point.
|
||||
one_normal: bool,
|
||||
textured: bool,
|
||||
}
|
||||
|
||||
const fn block(bit: u32, shape: Shape, one_normal: bool, textured: bool) -> Block {
|
||||
Block { bit, shape, one_normal, textured }
|
||||
}
|
||||
|
||||
/// The blocks in stream order, low bit first [WD 7.8.5.5].
|
||||
const BLOCKS: [Block; 12] = [
|
||||
block(0x0002, Shape::Triangles, false, false),
|
||||
block(0x0004, Shape::Fan, false, false),
|
||||
block(0x0008, Shape::Strip, false, false),
|
||||
block(0x0020, Shape::Triangles, true, false),
|
||||
block(0x0040, Shape::Fan, true, false),
|
||||
block(0x0080, Shape::Strip, true, false),
|
||||
block(0x0200, Shape::Triangles, false, true),
|
||||
block(0x0400, Shape::Fan, false, true),
|
||||
block(0x0800, Shape::Strip, false, true),
|
||||
block(0x2000, Shape::Triangles, true, true),
|
||||
block(0x4000, Shape::Fan, true, true),
|
||||
block(0x8000, Shape::Strip, true, true),
|
||||
];
|
||||
|
||||
/// One triangulation entity's index slots: per point its position, normal slot (when the
|
||||
/// array stores normals) and texture slots.
|
||||
#[derive(Default)]
|
||||
struct Slots {
|
||||
verts: Vec<u32>,
|
||||
normals: Vec<u32>,
|
||||
/// Per texture set, each point's slot.
|
||||
textures: Vec<Vec<u32>>,
|
||||
}
|
||||
|
||||
/// What a mesh's triangles carry besides positions, built in step with them: corner normal
|
||||
/// slots and, per texture set, corner texture slots (`None` for a triangle whose face stores
|
||||
/// fewer sets).
|
||||
#[derive(Default)]
|
||||
pub(super) struct Corners {
|
||||
pub(super) normals: Vec<[u32; 3]>,
|
||||
pub(super) textures: Vec<Vec<Option<[u32; 3]>>>,
|
||||
}
|
||||
|
||||
impl Face {
|
||||
/// Entity sizes per set block, from `TriangulatedData` [WD 7.8.6]. A triangle block is
|
||||
/// one size word (its triangle count); fans and strips list a vertex count each.
|
||||
fn entities(&self) -> Result<Vec<(Block, Vec<u32>)>, PrcError> {
|
||||
let mut d = self.data.iter().copied();
|
||||
let mut next = || d.next().ok_or_else(|| malformed("triangulated data ends early".into()));
|
||||
let mut out = Vec::new();
|
||||
for b in BLOCKS {
|
||||
if self.flags & b.bit == 0 {
|
||||
continue;
|
||||
}
|
||||
let sizes = match b.shape {
|
||||
Shape::Triangles => vec![next()?],
|
||||
Shape::Fan | Shape::Strip => {
|
||||
let n = next()? as usize;
|
||||
if n > self.data.len() {
|
||||
return Err(malformed("triangulated data ends early".into()));
|
||||
}
|
||||
(0..n).map(|_| next()).collect::<Result<_, _>>()?
|
||||
}
|
||||
};
|
||||
out.push((b, sizes));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Points the face's index slots name: the vertex-colour count (pdf-issues #820).
|
||||
pub(super) fn point_count(&self) -> Result<usize, PrcError> {
|
||||
let mut n = 0usize;
|
||||
for (b, sizes) in self.entities()? {
|
||||
for s in sizes {
|
||||
let s = (s & COUNT_MASK) as usize;
|
||||
n = n.saturating_add(if matches!(b.shape, Shape::Triangles) { s.saturating_mul(3) } else { s });
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Triangles [`Self::triangulate`] makes: one per triangle-block count, two fewer than the
|
||||
/// points of each fan and strip.
|
||||
pub(super) fn triangle_count(&self) -> Result<usize, PrcError> {
|
||||
let mut n = 0usize;
|
||||
for (b, sizes) in self.entities()? {
|
||||
for s in sizes {
|
||||
let s = (s & COUNT_MASK) as usize;
|
||||
n = n.saturating_add(if matches!(b.shape, Shape::Triangles) { s } else { s.saturating_sub(2) });
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// Texture sets the face's triangles carry: its count when one of its blocks is textured,
|
||||
/// at most [`MAX_TEXTURE_SETS`] (a face past it is refused when triangulated).
|
||||
pub(super) fn texture_sets(&self) -> usize {
|
||||
if BLOCKS.iter().any(|b| b.textured && self.flags & b.bit != 0) { self.textures.min(MAX_TEXTURE_SETS) } else { 0 }
|
||||
}
|
||||
|
||||
/// Emit the face's triangles into `out`; `normals` = the index array stores normal slots,
|
||||
/// in which case each triangle's corner normal slots go to `corners`, as do its corner
|
||||
/// texture slots. Returns the triangles each triangulation entity emitted, in order: one
|
||||
/// entity per triangle of a triangle block, per fan, per strip [WD 7.8.6].
|
||||
pub(super) fn triangulate(
|
||||
&self,
|
||||
indices: &[u32],
|
||||
normals: bool,
|
||||
n_points: usize,
|
||||
out: &mut Vec<[u32; 3]>,
|
||||
corners: &mut Corners,
|
||||
) -> Result<Vec<usize>, PrcError> {
|
||||
if self.textures > MAX_TEXTURE_SETS {
|
||||
return Err(malformed(format!("{} texture-coordinate sets on a face", self.textures)));
|
||||
}
|
||||
let mut entities = Vec::new();
|
||||
let mut slots = indices.get(self.start..).unwrap_or(&[]).iter().copied();
|
||||
for (b, sizes) in self.entities()? {
|
||||
let t = if b.textured { self.textures } else { 0 };
|
||||
for word in sizes {
|
||||
let count = (word & COUNT_MASK) as usize;
|
||||
// Points in this entity, and whether each carries its own normal slot.
|
||||
let (points, per_point_normal) = match b.shape {
|
||||
Shape::Triangles => (count.saturating_mul(3), !b.one_normal),
|
||||
Shape::Fan | Shape::Strip => (count, !b.one_normal || word & NORMAL_SINGLE == 0),
|
||||
};
|
||||
if points.saturating_mul(t.saturating_add(1)) > indices.len() {
|
||||
return Err(malformed("face runs past the triangulated index array".into()));
|
||||
}
|
||||
let s = read_slots(&mut slots, b.shape, points, t, normals.then_some(per_point_normal), n_points)?;
|
||||
let made = emit(b.shape, &s, out, corners);
|
||||
match b.shape {
|
||||
Shape::Triangles => entities.extend(std::iter::repeat_n(1, made)),
|
||||
Shape::Fan | Shape::Strip => entities.push(made),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(entities)
|
||||
}
|
||||
|
||||
/// Append one [`TriangleStyle`] per triangle of the face, whose entities emitted
|
||||
/// `entities[i]` triangles each. Entities past the face's styles inherit (style 0).
|
||||
pub(super) fn styles_into(&self, entities: &[usize], out: &mut Vec<TriangleStyle>) {
|
||||
let whole = match self.styles.as_slice() {
|
||||
[] => Some(0),
|
||||
[one] => Some(*one),
|
||||
_ => None,
|
||||
};
|
||||
for (i, &n) in entities.iter().enumerate() {
|
||||
let style = whole.unwrap_or_else(|| self.styles.get(i).copied().unwrap_or(0));
|
||||
out.extend(std::iter::repeat_n(TriangleStyle { style, behaviour: self.behaviour }, n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The 8-byte elements of [`crate::MAX_DECODED_ELEMENTS`] one triangle holds: its corners
|
||||
/// (12 bytes), its normal-slot corners when the index array stores them (12), a corner triple
|
||||
/// per texture set (16 each) and its style when the mesh is styled (8).
|
||||
pub(super) fn triangle_elements(normals: bool, sets: usize, styled: bool) -> usize {
|
||||
let normals = if normals { 2 } else { 0 };
|
||||
2usize.saturating_add(normals).saturating_add(sets.saturating_mul(2)).saturating_add(usize::from(styled))
|
||||
}
|
||||
|
||||
/// One entity's `points` index slots. `normals` is `None` when the array stores no normal
|
||||
/// slots, else whether every point has its own (rather than one per entity).
|
||||
fn read_slots(
|
||||
slots: &mut impl Iterator<Item = u32>,
|
||||
shape: Shape,
|
||||
points: usize,
|
||||
texture_sets: usize,
|
||||
normals: Option<bool>,
|
||||
n_points: usize,
|
||||
) -> Result<Slots, PrcError> {
|
||||
let mut take = || slots.next().ok_or_else(|| malformed("face runs past the triangulated index array".into()));
|
||||
// The caller bounded points * (texture_sets + 1) by the index array's length.
|
||||
let mut s = Slots { textures: vec![Vec::with_capacity(points); texture_sets], ..Slots::default() };
|
||||
let mut current = 0;
|
||||
for k in 0..points {
|
||||
let new_entity = match shape {
|
||||
Shape::Triangles => k % 3 == 0,
|
||||
Shape::Fan | Shape::Strip => k == 0,
|
||||
};
|
||||
if let Some(per_point) = normals {
|
||||
if per_point || new_entity {
|
||||
current = take()?;
|
||||
}
|
||||
s.normals.push(current);
|
||||
}
|
||||
for set in &mut s.textures {
|
||||
set.push(take()?);
|
||||
}
|
||||
let p = take()?;
|
||||
if !p.is_multiple_of(3) || p as usize / 3 >= n_points {
|
||||
return Err(malformed(format!("point index {p} is not a point")));
|
||||
}
|
||||
s.verts.push(p / 3);
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Append the triangles of one entity's `slots` to `out` and `corners`, keeping every
|
||||
/// texture set as long as `out`. Returns how many it made.
|
||||
fn emit(shape: Shape, slots: &Slots, out: &mut Vec<[u32; 3]>, corners: &mut Corners) -> usize {
|
||||
let before = out.len();
|
||||
assemble(shape, &slots.verts, out);
|
||||
assemble(shape, &slots.normals, &mut corners.normals);
|
||||
let made = out.len().saturating_sub(before);
|
||||
while corners.textures.len() < slots.textures.len() {
|
||||
corners.textures.push(vec![None; before]);
|
||||
}
|
||||
for (k, set) in corners.textures.iter_mut().enumerate() {
|
||||
match slots.textures.get(k) {
|
||||
Some(own) => {
|
||||
let mut tris = Vec::with_capacity(made);
|
||||
assemble(shape, own, &mut tris);
|
||||
set.extend(tris.into_iter().map(Some));
|
||||
}
|
||||
None => set.extend(std::iter::repeat_n(None, made)),
|
||||
}
|
||||
}
|
||||
made
|
||||
}
|
||||
|
||||
/// Append the triangles `shape` makes of `verts`.
|
||||
fn assemble(shape: Shape, verts: &[u32], out: &mut Vec<[u32; 3]>) {
|
||||
match shape {
|
||||
Shape::Triangles => out.extend(verts.as_chunks::<3>().0.iter().copied()),
|
||||
Shape::Fan => {
|
||||
if let Some((&c, rim)) = verts.split_first() {
|
||||
out.extend(rim.windows(2).filter_map(|w| match w {
|
||||
[a, b] => Some([c, *a, *b]),
|
||||
_ => None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Shape::Strip => out.extend(verts.windows(3).enumerate().filter_map(|(k, w)| match w {
|
||||
[a, b, c] if k % 2 == 0 => Some([*a, *b, *c]),
|
||||
[a, b, c] => Some([*b, *a, *c]),
|
||||
_ => None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Test-only PRC bit writer: the inverse of [`crate::BitReader`].
|
||||
|
||||
#![allow(clippy::expect_used)]
|
||||
|
||||
use crate::acof;
|
||||
|
||||
/// An MSB-first bit sink for building test streams.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct W {
|
||||
bits: Vec<bool>,
|
||||
}
|
||||
|
||||
impl W {
|
||||
/// The low `n` bits of `v` (`n` ≤ 64), most significant first.
|
||||
pub(crate) fn put(&mut self, v: u64, n: u32) -> &mut Self {
|
||||
for i in (0..n).rev() {
|
||||
self.bits.push((v >> i) & 1 == 1);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// One bit; a PRC `Boolean`.
|
||||
pub(crate) fn bit(&mut self, b: bool) -> &mut Self {
|
||||
self.bits.push(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Bits written so far.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.bits.len()
|
||||
}
|
||||
|
||||
/// Every bit of `other`, unaligned.
|
||||
pub(crate) fn append(&mut self, other: &Self) -> &mut Self {
|
||||
self.bits.extend_from_slice(&other.bits);
|
||||
self
|
||||
}
|
||||
|
||||
/// An `UnsignedInteger` [WD 11.10].
|
||||
pub(crate) fn uint(&mut self, mut v: u32) -> &mut Self {
|
||||
while v != 0 {
|
||||
self.bit(true).put(u64::from(v & 0xff), 8);
|
||||
v >>= 8;
|
||||
}
|
||||
self.bit(false)
|
||||
}
|
||||
|
||||
/// An `Integer` [WD 11.11]: two's-complement groups, low first, each followed by a
|
||||
/// continue bit.
|
||||
pub(crate) fn int(&mut self, v: i32) -> &mut Self {
|
||||
if v == 0 {
|
||||
return self.bit(false);
|
||||
}
|
||||
self.bit(true);
|
||||
let mut v = i64::from(v);
|
||||
loop {
|
||||
let group = v & 0xff;
|
||||
self.put(group.cast_unsigned(), 8);
|
||||
v >>= 8;
|
||||
let done = (v == 0 && group & 0x80 == 0) || (v == -1 && group & 0x80 != 0);
|
||||
self.bit(!done);
|
||||
if done {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `String` [WD 11.4]: presence bit, byte count, raw UTF-8 bytes.
|
||||
pub(crate) fn string(&mut self, s: Option<&str>) -> &mut Self {
|
||||
let Some(s) = s else {
|
||||
return self.bit(false);
|
||||
};
|
||||
self.bit(true).uint(u32::try_from(s.len()).expect("a short test string"));
|
||||
for b in s.bytes() {
|
||||
self.put(u64::from(b), 8);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// A finite `Double` [WD 11.17]: zero as the two-bit zero code; anything else as its
|
||||
/// exponent row, the sign, then the mantissa as literal bytes.
|
||||
pub(crate) fn double(&mut self, v: f64) -> &mut Self {
|
||||
if v == 0.0 {
|
||||
return self.put(0b01, 2);
|
||||
}
|
||||
let bits = v.to_bits();
|
||||
let exponent = u32::try_from((bits >> 52) & 0x7ff).expect("11 bits");
|
||||
let (n, c) = acof::exponent_code(exponent).expect("the Double table has a row per exponent");
|
||||
let le = bits.to_le_bytes();
|
||||
self.put(u64::from(c), n).bit(v < 0.0).bit(true).put(u64::from(le[6] & 0x0f), 4);
|
||||
for b in le[..6].iter().rev() {
|
||||
self.bit(true).put(u64::from(*b), 8);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// A Huffman container [WD 12.2]: `leaves` are `(symbol, stored code, length)`, each value
|
||||
/// is emitted as its leaf's code.
|
||||
pub(crate) fn huffman(&mut self, nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)], values: &[i32]) -> &mut Self {
|
||||
let mask = (1u32 << nbits) - 1;
|
||||
let codes: Vec<(u32, u32)> = values
|
||||
.iter()
|
||||
.map(|&v| {
|
||||
let l = leaves.iter().find(|l| l.0 & mask == v.cast_unsigned() & mask).expect("a leaf per value");
|
||||
(l.1, l.2)
|
||||
})
|
||||
.collect();
|
||||
self.huffman_raw(nbits, len_width, leaves, &codes)
|
||||
}
|
||||
|
||||
/// As [`Self::huffman`] with each element's `(code, length)` given.
|
||||
pub(crate) fn huffman_raw(&mut self, nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)], codes: &[(u32, u32)]) -> &mut Self {
|
||||
let mut blob = Self::huffman_head(nbits, len_width, leaves);
|
||||
lsb(&mut blob, u32::try_from(codes.len()).expect("a short test array"), 32);
|
||||
for &(c, l) in codes {
|
||||
for i in (0..l).rev() {
|
||||
blob.push((c >> i) & 1 == 1);
|
||||
}
|
||||
}
|
||||
self.huffman_words(blob)
|
||||
}
|
||||
|
||||
/// A Huffman container whose element count is `count` with no elements written.
|
||||
pub(crate) fn huffman_count(&mut self, nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)], count: u32) -> &mut Self {
|
||||
let mut blob = Self::huffman_head(nbits, len_width, leaves);
|
||||
lsb(&mut blob, count, 32);
|
||||
self.huffman_words(blob)
|
||||
}
|
||||
|
||||
fn huffman_head(nbits: u32, len_width: u32, leaves: &[(u32, u32, u32)]) -> Vec<bool> {
|
||||
let mut blob = Vec::new();
|
||||
lsb(&mut blob, u32::try_from(leaves.len()).expect("a short test table"), nbits + 1);
|
||||
lsb(&mut blob, len_width, 8);
|
||||
for &(s, c, l) in leaves {
|
||||
lsb(&mut blob, s, nbits);
|
||||
lsb(&mut blob, l, len_width);
|
||||
lsb(&mut blob, c, l);
|
||||
}
|
||||
blob
|
||||
}
|
||||
|
||||
/// The payload as whole 32-bit words of LSB-first bytes, then the last word's used bits.
|
||||
fn huffman_words(&mut self, mut blob: Vec<bool>) -> &mut Self {
|
||||
let bits = blob.len();
|
||||
let words = bits.div_ceil(32);
|
||||
blob.resize(words * 32, false);
|
||||
self.uint(u32::try_from(words).expect("a short test payload"));
|
||||
for byte in blob.chunks(8) {
|
||||
let b = byte.iter().enumerate().fold(0u64, |a, (i, &x)| a | (u64::from(x) << i));
|
||||
self.put(b, 8);
|
||||
}
|
||||
self.uint(u32::try_from(bits - (words - 1) * 32).expect("at most 32"))
|
||||
}
|
||||
|
||||
/// The bits packed MSB-first, the last byte zero-padded.
|
||||
pub(crate) fn bytes(&self) -> Vec<u8> {
|
||||
self.bits.chunks(8).map(|c| c.iter().enumerate().fold(0u8, |a, (i, &b)| a | (u8::from(b) << (7 - i)))).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The low `n` bits of `v`, least significant first.
|
||||
fn lsb(blob: &mut Vec<bool>, v: u32, n: u32) {
|
||||
for i in 0..n {
|
||||
blob.push((v >> i) & 1 == 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
//! What a tree carries beside its geometry, read past and not modelled: markup data (PMI)
|
||||
//! and views [WD 7.3.10.4, 7.7], and a scene's lights and clipping planes [WD 7.5.15-7.5.18,
|
||||
//! 7.11.13]. The tree section has no length to skip by, so each is read field by field.
|
||||
|
||||
use super::{
|
||||
AMBIENT_LIGHT, ANNOTATION_ITEM, ANNOTATION_REFERENCE, ANNOTATION_SET, DIRECTIONAL_LIGHT, LIGHT_INTENSITY_FROM, MARKUP_LINKED_ITEM, MAX_DEPTH,
|
||||
MKP_LEADER, MKP_MARKUP, MKP_VIEW, POINT_LIGHT, SPOT_LIGHT, SURF_PLANE, VIEW_FLAGS_FROM,
|
||||
};
|
||||
use crate::PrcError;
|
||||
use crate::entity::{Ctx, malformed};
|
||||
|
||||
impl Ctx<'_, '_> {
|
||||
/// `MarkupData` [WD 7.3.10.4]: linked items, leaders, markups, then annotation entities.
|
||||
pub(super) fn markup_data(&mut self) -> Result<(), PrcError> {
|
||||
let n = self.count(1, "markup linked items")?;
|
||||
for _ in 0..n {
|
||||
self.markup_linked_item()?;
|
||||
}
|
||||
let n = self.count(1, "leaders")?;
|
||||
for _ in 0..n {
|
||||
self.leader()?;
|
||||
}
|
||||
let n = self.count(1, "markups")?;
|
||||
for _ in 0..n {
|
||||
self.mkp_markup()?;
|
||||
}
|
||||
let n = self.count(1, "annotation entities")?;
|
||||
for _ in 0..n {
|
||||
self.annotation_entity(0)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A counted list of `MKP_View`s.
|
||||
pub(super) fn views(&mut self) -> Result<(), PrcError> {
|
||||
let n = self.count(1, "views")?;
|
||||
for _ in 0..n {
|
||||
self.view()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A counted list of `ReferenceOnPRCBase`s.
|
||||
fn references(&mut self, what: &'static str) -> Result<(), PrcError> {
|
||||
let n = self.count(1, what)?;
|
||||
for _ in 0..n {
|
||||
self.reference_on_prc_base()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `MISC_MarkupLinkedItem` (204) [WD 7.4.5].
|
||||
// prc-rs: a `has_reference_data` flag and its reference data follow the content entity
|
||||
// reference (pdf-issues #722).
|
||||
fn markup_linked_item(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(MARKUP_LINKED_ITEM)?;
|
||||
self.base_with_graphics()?;
|
||||
self.r.unsigned_integer()?; // local coordinate system + 1
|
||||
if self.r.bit()? {
|
||||
self.reference_data()?;
|
||||
}
|
||||
if self.r.bit()? {
|
||||
self.reference_data()?;
|
||||
}
|
||||
for _ in 0..4 {
|
||||
self.r.bit()?; // show and delete, markup and leader
|
||||
}
|
||||
self.schema.skip_added_fields(MARKUP_LINKED_ITEM, &mut self.r)?;
|
||||
self.user_data()
|
||||
}
|
||||
|
||||
/// `MKP_Leader` (503) [WD 7.7.5; pdf-issues #698].
|
||||
// prc-rs: the leader's field order.
|
||||
fn leader(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(MKP_LEADER)?;
|
||||
self.base_with_graphics()?;
|
||||
for _ in 0..2 {
|
||||
if self.r.bit()? {
|
||||
self.reference_on_prc_base()?; // first, second linked item
|
||||
}
|
||||
}
|
||||
self.r.unsigned_integer()?; // tessellation + 1
|
||||
self.schema.skip_added_fields(MKP_LEADER, &mut self.r)?;
|
||||
self.user_data()
|
||||
}
|
||||
|
||||
/// `MKP_Markup` (502) [WD 7.7.4].
|
||||
// prc-rs: the markup's field order.
|
||||
fn mkp_markup(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(MKP_MARKUP)?;
|
||||
self.base_with_graphics()?;
|
||||
self.r.unsigned_integer()?; // type
|
||||
self.r.unsigned_integer()?; // subtype
|
||||
self.references("markup linked items")?;
|
||||
self.references("markup leaders")?;
|
||||
self.r.unsigned_integer()?; // tessellation + 1
|
||||
self.schema.skip_added_fields(MKP_MARKUP, &mut self.r)?;
|
||||
self.user_data()
|
||||
}
|
||||
|
||||
/// An annotation item (504), set (505) or reference (506) [WD 7.7.6-7.7.8]; sets nest.
|
||||
// prc-rs: the annotation entities' field orders.
|
||||
fn annotation_entity(&mut self, depth: usize) -> Result<(), PrcError> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(malformed("annotation sets nest too deep".into()));
|
||||
}
|
||||
let t = self.r.unsigned_integer()?;
|
||||
if !matches!(t, ANNOTATION_ITEM | ANNOTATION_SET | ANNOTATION_REFERENCE) {
|
||||
return Err(malformed(format!("entity type {t} as an annotation")));
|
||||
}
|
||||
self.base_with_graphics()?;
|
||||
match t {
|
||||
ANNOTATION_ITEM => self.reference_on_prc_base()?,
|
||||
ANNOTATION_SET => {
|
||||
let n = self.count(1, "annotation set")?;
|
||||
for _ in 0..n {
|
||||
self.annotation_entity(depth + 1)?;
|
||||
}
|
||||
}
|
||||
_ => self.references("annotation reference")?,
|
||||
}
|
||||
self.schema.skip_added_fields(t, &mut self.r)?;
|
||||
self.user_data()
|
||||
}
|
||||
|
||||
/// `MKP_View` (501) [WD 7.7.3].
|
||||
// prc-rs: the view's field order, and its flags, linked items and filters from version
|
||||
// 8137.
|
||||
fn view(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(MKP_VIEW)?;
|
||||
self.base_with_graphics()?;
|
||||
self.references("view annotations")?;
|
||||
self.plane()?;
|
||||
if self.r.bit()? {
|
||||
self.scene_display_parameters()?;
|
||||
}
|
||||
if self.version >= VIEW_FLAGS_FROM {
|
||||
for _ in 0..3 {
|
||||
self.r.bit()?; // annotation view, default view, direction
|
||||
}
|
||||
self.references("view linked items")?;
|
||||
let n = self.count(1, "view filters")?;
|
||||
for _ in 0..n {
|
||||
self.filter()?;
|
||||
}
|
||||
}
|
||||
self.schema.skip_added_fields(MKP_VIEW, &mut self.r)?;
|
||||
self.user_data()
|
||||
}
|
||||
|
||||
/// `SURF_Plane` (86) [WD 7.11.13; pdf-issues #658]: `ContentSurface` [WD 7.11.3.2], a
|
||||
/// `Transformation3D` with no presence flag, the `Domain` [WD 8.2.4] and the
|
||||
/// parameterisation's four coefficients.
|
||||
// prc-rs: the plane's field order.
|
||||
pub(super) fn plane(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(SURF_PLANE)?;
|
||||
if self.r.bit()? {
|
||||
self.content_prc_base()?; // base geometry: attributes and name
|
||||
self.r.unsigned_integer()?; // identifier
|
||||
}
|
||||
self.r.unsigned_integer()?; // extend type
|
||||
self.transformation_3d()?;
|
||||
for _ in 0..8 {
|
||||
self.r.double()?; // domain (4), coefficients (4)
|
||||
}
|
||||
self.schema.skip_added_fields(SURF_PLANE, &mut self.r)
|
||||
}
|
||||
|
||||
/// A light (731-734) [WD 7.5.15-7.5.18].
|
||||
// prc-rs: the lights' field orders, and a directional light's intensity from version 8030
|
||||
// (pdf-issues #745).
|
||||
pub(super) fn light(&mut self) -> Result<(), PrcError> {
|
||||
let t = self.r.unsigned_integer()?;
|
||||
if !(AMBIENT_LIGHT..=SPOT_LIGHT).contains(&t) {
|
||||
return Err(malformed(format!("entity type {t} as a light")));
|
||||
}
|
||||
self.content_prc_ref_base()?;
|
||||
for _ in 0..3 {
|
||||
self.r.unsigned_integer()?; // ambient, diffuse, specular colour + 1
|
||||
}
|
||||
if matches!(t, POINT_LIGHT | SPOT_LIGHT) {
|
||||
self.vector3()?; // location
|
||||
for _ in 0..3 {
|
||||
self.r.double()?; // attenuation
|
||||
}
|
||||
}
|
||||
if matches!(t, DIRECTIONAL_LIGHT | SPOT_LIGHT) {
|
||||
self.vector3()?; // direction
|
||||
}
|
||||
if t == DIRECTIONAL_LIGHT && self.version >= LIGHT_INTENSITY_FROM {
|
||||
self.r.double()?;
|
||||
}
|
||||
if t == SPOT_LIGHT {
|
||||
self.r.double()?; // fall-off angle
|
||||
self.r.double()?; // fall-off exponent
|
||||
}
|
||||
self.schema.skip_added_fields(t, &mut self.r)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use super::tests::{Occ, PLAIN, S_RED, base, graphics, part_with, plate, ref_base, single};
|
||||
use crate::PrcError;
|
||||
use crate::bits::BitReader;
|
||||
use crate::entity::Ctx;
|
||||
use crate::testw::W;
|
||||
use crate::{Schema, Tessellation};
|
||||
|
||||
/// Read after each entity, to show it was read to its last bit and no further.
|
||||
const SENTINEL: u32 = 0x5A5A;
|
||||
|
||||
/// Runs `read` over `w` then the sentinel, at authoring version `version`.
|
||||
fn read_at(w: &W, version: u32, read: impl FnOnce(&mut Ctx<'_, '_>) -> Result<(), PrcError>) -> Result<(), PrcError> {
|
||||
let mut all = W::default();
|
||||
all.append(w).uint(SENTINEL);
|
||||
let bytes = all.bytes();
|
||||
let schema = Schema::default();
|
||||
let mut ctx = Ctx::new(BitReader::new(&bytes), &schema, version);
|
||||
read(&mut ctx)?;
|
||||
assert_eq!(ctx.r.unsigned_integer()?, SENTINEL, "read past the wrong number of bits");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(w: &W, read: impl FnOnce(&mut Ctx<'_, '_>) -> Result<(), PrcError>) -> Result<(), PrcError> {
|
||||
read_at(w, 8137, read)
|
||||
}
|
||||
|
||||
/// Each truncation of `w` is an error, never a panic.
|
||||
fn truncations_fail(w: &W, read: impl Fn(&mut Ctx<'_, '_>) -> Result<(), PrcError>) {
|
||||
let bytes = w.bytes();
|
||||
let schema = Schema::default();
|
||||
for end in 0..bytes.len().saturating_sub(1) {
|
||||
let mut ctx = Ctx::new(BitReader::new(&bytes[..end]), &schema, 8137);
|
||||
assert!(read(&mut ctx).is_err(), "cut at {end}");
|
||||
}
|
||||
}
|
||||
|
||||
fn reference(w: &mut W, uid: u32) {
|
||||
w.uint(205).uint(237).bit(true).uint(uid);
|
||||
}
|
||||
|
||||
fn markup_data(linked: &[W], leaders: &[W], markups: &[W], annotations: &[W]) -> W {
|
||||
let mut w = W::default();
|
||||
for list in [linked, leaders, markups, annotations] {
|
||||
w.uint(u32::try_from(list.len()).unwrap());
|
||||
for e in list {
|
||||
w.append(e);
|
||||
}
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
fn linked_item() -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(204);
|
||||
graphics(&mut w, None, 1, PLAIN);
|
||||
w.uint(0).bit(true);
|
||||
reference(&mut w, 7);
|
||||
w.bit(true).uint(206).uint(149).bit(true).bit(true).uint(1).uint(1).uint(1).uint(0);
|
||||
w.bit(true).bit(false).bit(true).bit(false).uint(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn leader() -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(503);
|
||||
graphics(&mut w, None, 2, PLAIN);
|
||||
w.bit(true);
|
||||
reference(&mut w, 3);
|
||||
w.bit(false).uint(4).uint(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn markup() -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(502);
|
||||
graphics(&mut w, Some("Note"), 3, PLAIN);
|
||||
w.uint(1).uint(2).uint(1);
|
||||
reference(&mut w, 7);
|
||||
w.uint(2);
|
||||
reference(&mut w, 2);
|
||||
reference(&mut w, 2);
|
||||
w.uint(3).uint(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn annotation(t: u32, inner: impl FnOnce(&mut W)) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(t);
|
||||
graphics(&mut w, None, 4, PLAIN);
|
||||
inner(&mut w);
|
||||
w.uint(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn annotation_item() -> W {
|
||||
annotation(504, |w| reference(w, 3))
|
||||
}
|
||||
|
||||
fn annotation_set(members: &[W]) -> W {
|
||||
annotation(505, |w| {
|
||||
w.uint(u32::try_from(members.len()).unwrap());
|
||||
for m in members {
|
||||
w.append(m);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn annotation_reference() -> W {
|
||||
annotation(506, |w| {
|
||||
w.uint(1);
|
||||
reference(w, 3);
|
||||
})
|
||||
}
|
||||
|
||||
fn plane(w: &mut W) {
|
||||
w.uint(86).bit(true);
|
||||
base(w, Some("Front"));
|
||||
w.uint(9).uint(0).put(0x01, 8);
|
||||
for v in [1., 2., 3., 0., 0., 1., 1., 1., 1., 0., 0.] {
|
||||
w.double(v);
|
||||
}
|
||||
}
|
||||
|
||||
fn light(t: u32) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(t);
|
||||
ref_base(&mut w, None, 5);
|
||||
w.uint(1).uint(2).uint(3);
|
||||
let doubles = match t {
|
||||
732 => 6,
|
||||
733 => 4,
|
||||
734 => 11,
|
||||
_ => 0,
|
||||
};
|
||||
for _ in 0..doubles {
|
||||
w.double(0.5);
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
/// A scene with `lights` and `planes` clipping planes; `is_absolute` is written from 8137.
|
||||
fn scene(lights: &[W], planes: usize, version: u32) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(741);
|
||||
ref_base(&mut w, None, 6);
|
||||
w.bit(true).uint(u32::try_from(lights.len()).unwrap());
|
||||
for l in lights {
|
||||
w.append(l);
|
||||
}
|
||||
w.bit(false).bit(false).uint(u32::try_from(planes).unwrap());
|
||||
for _ in 0..planes {
|
||||
plane(&mut w);
|
||||
}
|
||||
w.uint(0).uint(0).uint(0);
|
||||
if version >= 8137 {
|
||||
w.bit(true);
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
fn filter(w: &mut W) {
|
||||
w.uint(320);
|
||||
ref_base(w, None, 8);
|
||||
w.bit(true).bit(true).uint(0).bit(true).uint(0).uint(0);
|
||||
}
|
||||
|
||||
fn view(version: u32) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(501);
|
||||
graphics(&mut w, Some("Top"), 7, PLAIN);
|
||||
w.uint(1);
|
||||
reference(&mut w, 3);
|
||||
plane(&mut w);
|
||||
w.bit(true).append(&scene(&[light(731)], 1, version));
|
||||
if version >= 8137 {
|
||||
w.bit(true).bit(false).bit(true).uint(1);
|
||||
reference(&mut w, 7);
|
||||
w.uint(1);
|
||||
filter(&mut w);
|
||||
}
|
||||
w.uint(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn views(vs: &[W]) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(u32::try_from(vs.len()).unwrap());
|
||||
for v in vs {
|
||||
w.append(v);
|
||||
}
|
||||
w
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_markup_linked_item_is_read_past() {
|
||||
let w = markup_data(&[linked_item()], &[], &[], &[]);
|
||||
read(&w, |c| c.markup_data()).unwrap();
|
||||
truncations_fail(&w, |c| c.markup_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_leader_is_read_past() {
|
||||
let w = markup_data(&[], &[leader()], &[], &[]);
|
||||
read(&w, |c| c.markup_data()).unwrap();
|
||||
truncations_fail(&w, |c| c.markup_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_markup_is_read_past() {
|
||||
let w = markup_data(&[], &[], &[markup()], &[]);
|
||||
read(&w, |c| c.markup_data()).unwrap();
|
||||
truncations_fail(&w, |c| c.markup_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation_items_sets_and_references_are_read_past() {
|
||||
let set = annotation_set(&[annotation_item(), annotation_set(&[annotation_reference()])]);
|
||||
let w = markup_data(&[], &[], &[], &[annotation_item(), set, annotation_reference()]);
|
||||
read(&w, |c| c.markup_data()).unwrap();
|
||||
truncations_fail(&w, |c| c.markup_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation_sets_nested_past_the_ceiling_are_an_error() {
|
||||
let mut e = annotation_item();
|
||||
for _ in 0..=super::MAX_DEPTH {
|
||||
e = annotation_set(&[e]);
|
||||
}
|
||||
let w = markup_data(&[], &[], &[], &[e]);
|
||||
assert!(matches!(read(&w, |c| c.markup_data()), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_annotation_of_another_type_is_an_error() {
|
||||
let w = markup_data(&[], &[], &[], &[annotation(502, |_| {})]);
|
||||
assert!(matches!(read(&w, |c| c.markup_data()), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_view_is_read_past_with_its_flags_only_from_8137() {
|
||||
let w = views(&[view(8137)]);
|
||||
read(&w, |c| c.views()).unwrap();
|
||||
truncations_fail(&w, |c| c.views());
|
||||
read_at(&views(&[view(8030)]), 8030, |c| c.views()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_lights_of_each_kind_are_read_past() {
|
||||
let w = scene(&[light(731), light(732), light(733), light(734)], 0, 8137);
|
||||
read(&w, |c| c.scene_display_parameters()).unwrap();
|
||||
truncations_fail(&w, |c| c.scene_display_parameters());
|
||||
let mut old = W::default();
|
||||
old.uint(733);
|
||||
ref_base(&mut old, None, 5);
|
||||
old.uint(1).uint(2).uint(3).double(0.).double(0.).double(1.);
|
||||
read_at(&old, 8020, |c| c.light()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_clipping_planes_are_read_past() {
|
||||
let w = scene(&[], 2, 8137);
|
||||
read(&w, |c| c.scene_display_parameters()).unwrap();
|
||||
truncations_fail(&w, |c| c.scene_display_parameters());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tree_with_markups_and_views_still_places_its_parts() {
|
||||
let mut tail = markup_data(&[linked_item()], &[leader()], &[markup()], &[annotation_set(&[annotation_item()])]);
|
||||
tail.append(&views(&[view(8137)]));
|
||||
let part = part_with(Some("Plate"), PLAIN, 1, |w| super::tests::poly_item(w, 7, 0, 1, (S_RED, super::SHOW)), &tail);
|
||||
let occs = [Occ { name: Some("Asm"), sons: vec![1], markups: Some(tail.clone()), ..Occ::default() }, Occ { part: 1, ..Occ::default() }];
|
||||
let f = single(&[part, plate(S_RED)], &occs);
|
||||
let nodes = f.model_tree().unwrap();
|
||||
assert_eq!(nodes.len(), 2);
|
||||
let p = f.placements().unwrap();
|
||||
assert_eq!(p.len(), 1);
|
||||
assert!(matches!(f.file_structures[0].tessellations().unwrap().first(), Some(Tessellation::Mesh(_))));
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
//! 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 (PMI), views, scene lights and clipping planes are read past and not modelled.
|
||||
|
||||
// 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, Budget};
|
||||
use crate::container::offset;
|
||||
use crate::entity::{Ctx, malformed};
|
||||
use crate::geometry::{Matrix, Vec3};
|
||||
use crate::{PrcError, PrcFile, Schema, SectionKind, UniqueId};
|
||||
|
||||
mod markup;
|
||||
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 SURF_PLANE: u32 = 86;
|
||||
const CARTESIAN_TRANSFORMATION: u32 = 202;
|
||||
const ENTITY_REFERENCE: u32 = 203;
|
||||
const MARKUP_LINKED_ITEM: u32 = 204;
|
||||
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 MKP_VIEW: u32 = 501;
|
||||
const MKP_MARKUP: u32 = 502;
|
||||
const MKP_LEADER: u32 = 503;
|
||||
const ANNOTATION_ITEM: u32 = 504;
|
||||
const ANNOTATION_SET: u32 = 505;
|
||||
const ANNOTATION_REFERENCE: u32 = 506;
|
||||
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 AMBIENT_LIGHT: u32 = 731;
|
||||
const POINT_LIGHT: u32 = 732;
|
||||
const DIRECTIONAL_LIGHT: u32 = 733;
|
||||
const SPOT_LIGHT: u32 = 734;
|
||||
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;
|
||||
/// prc-rs: a view's flags, linked items and filters exist from this authoring version.
|
||||
const VIEW_FLAGS_FROM: u32 = 8137;
|
||||
/// prc-rs: a directional light's intensity exists from this authoring version.
|
||||
const LIGHT_INTENSITY_FROM: u32 = 8030;
|
||||
/// `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),
|
||||
/// as the tree stores it: [`PrcFile::placements`] does not decode that section, so an
|
||||
/// index past its end is the caller's to refuse.
|
||||
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`] when an item or an entity reference names a reference
|
||||
/// coordinate system and the globals carry fonts or fill patterns; [`PrcError::Malformed`] for a tree 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> {
|
||||
self.walk_within(rule, scope, Budget::FULL)
|
||||
}
|
||||
|
||||
/// [`Self::walk`] with `budget` shared by all its readers.
|
||||
pub(crate) fn walk_within(&self, rule: StyleAlpha, scope: EntityOverrides, budget: Budget) -> Result<(Vec<Placement>, Vec<ModelNode>), PrcError> {
|
||||
if !self.stores_tree() {
|
||||
return Ok((Vec::new(), Vec::new()));
|
||||
}
|
||||
// One allowance for the whole walk, so many sections cannot each spend a full one.
|
||||
let mut left = budget;
|
||||
let mut trees = Vec::with_capacity(self.file_structures.len());
|
||||
for fs in &self.file_structures {
|
||||
// The globals open with the schema every reader of this structure uses: read it
|
||||
// once, charged to the walk, and go on from there for the globals themselves.
|
||||
let mut globals_reader = BitReader::with_budget(fs.section(SectionKind::Globals), left);
|
||||
let schema = Schema::read(&mut globals_reader)?;
|
||||
left = globals_reader.budget();
|
||||
let mut ctx = Ctx::new(BitReader::with_budget(fs.section(SectionKind::Tree), left), &schema, fs.authoring_version);
|
||||
let tree = ctx.file_structure_tree()?;
|
||||
left = ctx.r.budget();
|
||||
// The globals carry the coordinate systems and the colours; colours alone are not
|
||||
// worth refusing the placements over.
|
||||
let mut ctx = Ctx::new(globals_reader.with_remaining(left), &schema, fs.authoring_version);
|
||||
let globals = ctx.globals();
|
||||
left = ctx.r.budget();
|
||||
let globals = match globals {
|
||||
Ok(g) => g,
|
||||
Err(e) if places_by_system(&tree) => return Err(e),
|
||||
Err(_) => Globals::default(),
|
||||
};
|
||||
trees.push((fs.id, tree, globals));
|
||||
}
|
||||
let mut r = BitReader::with_budget(&self.model_file, left);
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an item of `tree`, or an entity reference one of its occurrences holds, names a
|
||||
/// coordinate system: then the globals that store them must have been read.
|
||||
fn places_by_system(tree: &Tree) -> bool {
|
||||
tree.parts.iter().flatten().any(|i| !i.local.is_empty()) || tree.products.iter().flat_map(|p| &p.overrides).any(|o| o.local != 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod markup_tests;
|
||||
#[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,452 @@
|
||||
//! 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.
|
||||
pub(super) 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
|
||||
/// included.
|
||||
// prc-rs: the scene's field order, and `is_absolute` from version 8137.
|
||||
pub(super) fn scene_display_parameters(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(SCENE_DISPLAY_PARAMETERS)?;
|
||||
self.content_prc_ref_base()?;
|
||||
self.r.bit()?; // is_active
|
||||
let n = self.count(1, "scene lights")?;
|
||||
for _ in 0..n {
|
||||
self.light()?;
|
||||
}
|
||||
if self.r.bit()? {
|
||||
self.camera()?;
|
||||
}
|
||||
if self.r.bit()? {
|
||||
self.vector3()?; // rotation centre
|
||||
}
|
||||
let n = self.count(1, "scene clipping planes")?;
|
||||
for _ in 0..n {
|
||||
self.plane()?;
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
/// `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 target = if self.r.bit()? { self.reference_data()? } else { None };
|
||||
self.schema.skip_added_fields(ENTITY_REFERENCE, &mut self.r)?;
|
||||
self.user_data()?;
|
||||
Ok(target.map(|target| Override { graphics, hidden, local, target }))
|
||||
}
|
||||
|
||||
/// `ReferenceData` [WD 7.4.10.2]: a `ReferenceOnPRCBase` (205) or a
|
||||
/// `ReferenceOnTopology` (206), type code included; `None` when it names no target.
|
||||
pub(super) fn reference_data(&mut self) -> Result<Option<Target>, PrcError> {
|
||||
let t = self.r.unsigned_integer()?;
|
||||
let target = match t {
|
||||
REFERENCE_ON_PRC_BASE => Some(self.reference_on_prc_base_body()?),
|
||||
REFERENCE_ON_TOPOLOGY => self.reference_on_topology()?,
|
||||
t => return Err(malformed(format!("entity type {t} as reference data"))),
|
||||
};
|
||||
self.schema.skip_added_fields(t, &mut self.r)?;
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
/// `ReferenceOnPRCBase` (205) [WD 7.4.6] after its type code.
|
||||
fn reference_on_prc_base_body(&mut self) -> Result<Target, PrcError> {
|
||||
let kind = self.r.unsigned_integer()?;
|
||||
let fs = self.file_identifier()?;
|
||||
let uid = self.r.unsigned_integer()?;
|
||||
Ok(Target::Entity { kind, fs, uid })
|
||||
}
|
||||
|
||||
/// `ReferenceOnPRCBase` (205) [WD 7.4.6], type code included: read past.
|
||||
pub(super) fn reference_on_prc_base(&mut self) -> Result<(), PrcError> {
|
||||
self.expect_type(REFERENCE_ON_PRC_BASE)?;
|
||||
self.reference_on_prc_base_body()?;
|
||||
self.schema.skip_added_fields(REFERENCE_ON_PRC_BASE, &mut self.r)
|
||||
}
|
||||
|
||||
/// `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.
|
||||
pub(super) 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,714 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use super::style::{Material, Style};
|
||||
use super::{
|
||||
EntityOverrides, FATHER_HERIT_COLOR, FILE_STRUCTURE_TREE, Globals, Graphics, MAX_PLACEMENTS, NameSource, SHOW, SON_HERIT_COLOR, StyleAlpha,
|
||||
resolve_style,
|
||||
};
|
||||
use crate::bits::{BitReader, Budget};
|
||||
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.
|
||||
pub(super) const PLAIN: (u32, u16) = (0, SHOW);
|
||||
/// The biased style indices of [`palette`]: red, green, a blue material.
|
||||
pub(super) 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.
|
||||
pub(super) fn base(w: &mut W, name: Option<&str>) {
|
||||
w.uint(0).bit(false).string(name);
|
||||
}
|
||||
|
||||
pub(super) 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)`.
|
||||
pub(super) 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`.
|
||||
pub(super) 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, and no markups or views.
|
||||
fn part(name: Option<&str>, g: (u32, u16), n: usize, items: impl FnOnce(&mut W)) -> W {
|
||||
let mut none = W::default();
|
||||
for _ in 0..5 {
|
||||
none.uint(0);
|
||||
}
|
||||
part_with(name, g, n, items, &none)
|
||||
}
|
||||
|
||||
/// [`part`] with `markups` (its markup data and views) written after the items.
|
||||
pub(super) fn part_with(name: Option<&str>, g: (u32, u16), n: usize, items: impl FnOnce(&mut W), markups: &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);
|
||||
w.append(markups).uint(0); // user data
|
||||
w
|
||||
}
|
||||
|
||||
/// One poly-B-rep item (uid 7) drawing tessellation 1 in style `style`.
|
||||
pub(super) fn plate(style: u32) -> W {
|
||||
part(Some("Plate"), PLAIN, 1, |w| poly_item(w, 7, 0, 1, (style, SHOW)))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Occ<'a> {
|
||||
pub(super) name: Option<&'a str>,
|
||||
pub(super) g: Option<(u32, u16)>,
|
||||
pub(super) part: u32,
|
||||
pub(super) prototype: u32,
|
||||
pub(super) prototype_fs: Option<usize>,
|
||||
pub(super) sons: Vec<u32>,
|
||||
pub(super) suppressed: bool,
|
||||
pub(super) location: Option<W>,
|
||||
pub(super) refs: Vec<W>,
|
||||
/// Its markup data and views; none when `None`.
|
||||
pub(super) markups: Option<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);
|
||||
}
|
||||
match &o.markups {
|
||||
Some(m) => {
|
||||
w.append(m);
|
||||
}
|
||||
None => {
|
||||
for _ in 0..5 {
|
||||
w.uint(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
w.bit(false).uint(0).uint(0).uint(0);
|
||||
}
|
||||
|
||||
pub(super) 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 schema = W::default();
|
||||
schema.uint(0);
|
||||
globals_after(schema, colours, materials, styles, systems)
|
||||
}
|
||||
|
||||
/// [`globals`] after the already-written `schema`.
|
||||
fn globals_after(mut w: W, colours: &[Rgba], materials: &[W], styles: &[W], systems: &[W]) -> W {
|
||||
w.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 {
|
||||
ref_entity_at(uid, g, 0)
|
||||
}
|
||||
|
||||
/// [`ref_entity`], also placing its target by biased coordinate system `local`.
|
||||
fn ref_entity_at(uid: u32, g: (u32, u16), local: u32) -> W {
|
||||
let mut w = W::default();
|
||||
w.uint(203);
|
||||
graphics(&mut w, None, 0, g);
|
||||
w.uint(local).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.
|
||||
pub(super) 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"));
|
||||
}
|
||||
|
||||
/// One element allowance covers the whole walk: an allowance that fits one file structure does
|
||||
/// not fit two, and running short of it is reported as such.
|
||||
#[test]
|
||||
fn one_element_allowance_covers_the_whole_walk() {
|
||||
// A part of 64 items, so each file structure's tree section, not its globals or the model
|
||||
// file, decodes most of the elements.
|
||||
let wide = || part(None, PLAIN, 64, |w| (0..64).for_each(|_| poly_item(w, 0, 0, 1, PLAIN)));
|
||||
let fs = || Fs { globals: palette(), tree: tree(&[wide()], &[Occ { part: 1, ..Occ::default() }]), tess: two_face_square() };
|
||||
let one = PrcFile::parse(&prc(&[fs()], &model(&[(0, 1)]))).unwrap();
|
||||
let two = PrcFile::parse(&prc(&[fs(), fs()], &model(&[(0, 1), (1, 1)]))).unwrap();
|
||||
let walk = |f: &PrcFile, n| f.walk_within(StyleAlpha::default(), EntityOverrides::default(), Budget { elements: n, ..Budget::FULL });
|
||||
let short = |f: &PrcFile, n| matches!(walk(f, n), Err(PrcError::TooManyElements { .. }));
|
||||
// One structure decodes about 70 elements, two about 140.
|
||||
assert!(short(&one, 32));
|
||||
assert_eq!(walk(&one, 100).map(|w| w.0.len()), Ok(64));
|
||||
assert!(short(&two, 100), "each structure fits in 100, both do not");
|
||||
assert_eq!(walk(&two, 200).map(|w| w.0.len()), Ok(128));
|
||||
}
|
||||
|
||||
/// One schema-step allowance covers the whole walk: each structure's tree runs a 2,000-step
|
||||
/// program, which a 3,000-step allowance fits once but not twice.
|
||||
#[test]
|
||||
fn one_step_allowance_covers_the_whole_walk() {
|
||||
let fs = || {
|
||||
let mut schema = W::default();
|
||||
// For (1000) { empty block } on the file structure tree.
|
||||
schema.uint(1).uint(FILE_STRUCTURE_TREE).uint(5);
|
||||
for t in [15, 26, 1000, 19, 21] {
|
||||
schema.uint(t);
|
||||
}
|
||||
let globals = globals_after(schema, &[RED], &[], &[], &[]);
|
||||
Fs { globals, tree: tree(&[plate(S_RED)], &[Occ { part: 1, ..Occ::default() }]), tess: two_face_square() }
|
||||
};
|
||||
let one = PrcFile::parse(&prc(&[fs()], &model(&[(0, 1)]))).unwrap();
|
||||
let two = PrcFile::parse(&prc(&[fs(), fs()], &model(&[(0, 1), (1, 1)]))).unwrap();
|
||||
let walk = |f: &PrcFile| f.walk_within(StyleAlpha::default(), EntityOverrides::default(), Budget { steps: 3000, ..Budget::FULL });
|
||||
assert_eq!(walk(&one).map(|w| w.0.len()), Ok(1));
|
||||
assert!(matches!(walk(&two), Err(PrcError::Malformed(m)) if m.contains("step ceiling")));
|
||||
assert_eq!(two.placements().map(|p| p.len()), Ok(2));
|
||||
}
|
||||
|
||||
/// Each structure's schema is read once, charged to the walk's allowance: a schema too large
|
||||
/// for what is left is an error, not decoded outside the allowance and then skipped.
|
||||
#[test]
|
||||
fn the_globals_schema_is_charged_to_the_walk() {
|
||||
let mut schema = W::default();
|
||||
// 200 empty entries cost 200 elements.
|
||||
schema.uint(200);
|
||||
for t in 0..200 {
|
||||
schema.uint(50_000 + t).uint(0);
|
||||
}
|
||||
let fs = Fs {
|
||||
globals: globals_after(schema, &[RED], &[], &[], &[]),
|
||||
tree: tree(&[plate(S_RED)], &[Occ { part: 1, ..Occ::default() }]),
|
||||
tess: two_face_square(),
|
||||
};
|
||||
let f = PrcFile::parse(&prc(&[fs], &model(&[(0, 1)]))).unwrap();
|
||||
let walk = |n| f.walk_within(StyleAlpha::default(), EntityOverrides::default(), Budget { elements: n, ..Budget::FULL });
|
||||
assert!(matches!(walk(100), Err(PrcError::TooManyElements { .. })));
|
||||
assert_eq!(walk(1000).map(|w| w.0.len()), Ok(1));
|
||||
}
|
||||
|
||||
/// Globals that cannot be read are reported, not replaced by empty ones, when an entity
|
||||
/// reference names one of their coordinate systems.
|
||||
#[test]
|
||||
fn unreadable_globals_are_an_error_when_a_reference_names_a_coordinate_system() {
|
||||
let file = |local| {
|
||||
let mut fonts = W::default();
|
||||
fonts.uint(0).uint(303);
|
||||
base(&mut fonts, None);
|
||||
fonts.uint(0).double(0.).double(0.).string(None).uint(1);
|
||||
let occs = [Occ { part: 1, refs: vec![ref_entity_at(7, PLAIN, local)], ..Occ::default() }];
|
||||
let fs = Fs { globals: fonts, tree: tree(&[plate(S_RED)], &occs), tess: two_face_square() };
|
||||
PrcFile::parse(&prc(&[fs], &model(&[(0, 1)]))).unwrap()
|
||||
};
|
||||
assert_eq!(file(0).placements().map(|p| p.len()), Ok(1));
|
||||
assert!(matches!(file(1).placements(), Err(PrcError::Unsupported("PRC global fonts"))));
|
||||
}
|
||||
|
||||
#[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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prototype_chain_past_the_ceiling_is_an_error() {
|
||||
// Occurrence k (1-based) has occurrence k + 1 as its prototype; none has a part.
|
||||
let n = u32::try_from(super::MAX_DEPTH).unwrap() + 3;
|
||||
let occs: Vec<_> = (1..=n).map(|k| Occ { prototype: if k < n { k + 1 } else { 0 }, ..Occ::default() }).collect();
|
||||
assert!(malformed(single(&[], &occs).model_tree()).contains("prototype chain too long"));
|
||||
}
|
||||
|
||||
/// `depth` representation sets, each holding the next, around one poly-B-rep item.
|
||||
fn nested_sets(w: &mut W, depth: usize) {
|
||||
if depth == 0 {
|
||||
return poly_item(w, 0, 0, 1, PLAIN);
|
||||
}
|
||||
w.uint(super::RI_SET);
|
||||
graphics(w, None, 0, PLAIN);
|
||||
w.uint(0).uint(0).uint(1);
|
||||
nested_sets(w, depth - 1);
|
||||
w.uint(0); // user data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn representation_sets_nested_past_the_ceiling_are_an_error() {
|
||||
let deep = part(None, PLAIN, 1, |w| nested_sets(w, super::MAX_DEPTH + 1));
|
||||
let occs = [Occ { part: 1, ..Occ::default() }];
|
||||
assert!(malformed(single(&[deep], &occs).placements()).contains("representation sets nest too deep"));
|
||||
let ok = part(None, PLAIN, 1, |w| nested_sets(w, super::MAX_DEPTH));
|
||||
assert_eq!(single(&[ok], &occs).placements().unwrap().len(), 1);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! The PRC container walk [WD 6.1-6.2] against synthetic streams.
|
||||
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use pdfcraft_prc::{MAX_FILE_STRUCTURES, PrcError, PrcFile, SectionKind, UniqueId};
|
||||
|
||||
fn zlib(data: &[u8]) -> Vec<u8> {
|
||||
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]) {
|
||||
for v in vs {
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// One file structure with the given section payloads and model file.
|
||||
fn build(sections: [&[u8]; 5], model: &[u8], padding: usize) -> Vec<u8> {
|
||||
const HEADER_LEN: usize = 107;
|
||||
let mut fs_header = b"PRC".to_vec();
|
||||
u32s(&mut fs_header, &[8137, 8137, 5, 6, 7, 8, 0, 0, 0, 0, 1, 3]);
|
||||
fs_header.extend_from_slice(b"\xff\xd8\xff");
|
||||
|
||||
let mut body = fs_header.clone();
|
||||
let mut offs = vec![HEADER_LEN];
|
||||
for s in sections {
|
||||
offs.push(HEADER_LEN + body.len());
|
||||
body.extend(zlib(s));
|
||||
body.extend(std::iter::repeat_n(0u8, padding));
|
||||
}
|
||||
let mf_start = HEADER_LEN + body.len();
|
||||
body.extend(zlib(model));
|
||||
let mf_end = HEADER_LEN + body.len();
|
||||
|
||||
let mut out = b"PRC".to_vec();
|
||||
u32s(&mut out, &[8137, 8137, 1, 2, 3, 4, 0, 0, 0, 0, 1]);
|
||||
u32s(&mut out, &[5, 6, 7, 8, 0, 6]);
|
||||
u32s(&mut out, &offs.iter().map(|&o| o as u32).collect::<Vec<_>>());
|
||||
u32s(&mut out, &[mf_start as u32, mf_end as u32, 0]);
|
||||
assert_eq!(out.len(), HEADER_LEN);
|
||||
out.extend(body);
|
||||
out
|
||||
}
|
||||
|
||||
const SECTIONS: [&[u8]; 5] = [b"globals", b"tree", b"tess", b"", b"extra"];
|
||||
|
||||
#[test]
|
||||
fn the_container_walk_inflates_every_section() {
|
||||
for padding in [0, 3] {
|
||||
let f = PrcFile::parse(&build(SECTIONS, b"model file", padding)).unwrap();
|
||||
assert_eq!(f.header.min_version_for_read, 8137);
|
||||
assert_eq!(f.header.file_id, UniqueId([1, 2, 3, 4]));
|
||||
assert_eq!(f.file_structures.len(), 1);
|
||||
let fs = &f.file_structures[0];
|
||||
assert_eq!(fs.id, UniqueId([5, 6, 7, 8]));
|
||||
assert_eq!(fs.pictures, vec![b"\xff\xd8\xff".to_vec()]);
|
||||
for (kind, want) in SectionKind::ALL.into_iter().zip(SECTIONS) {
|
||||
assert_eq!(fs.section(kind), want, "{}", kind.name());
|
||||
}
|
||||
assert_eq!(f.model_file, b"model file");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_containers_are_refused_by_name() {
|
||||
let good = build(SECTIONS, b"m", 0);
|
||||
assert_eq!(PrcFile::parse(b"U3D\0").unwrap_err(), PrcError::NotPrc);
|
||||
assert!(matches!(PrcFile::parse(&good[..40]), Err(PrcError::Truncated(_))));
|
||||
|
||||
// Reserved field (offset 63) not zero.
|
||||
let mut bad = good.clone();
|
||||
bad[63] = 1;
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
|
||||
|
||||
// File-structure id disagrees with its description (FS header id at 107+11).
|
||||
let mut bad = good.clone();
|
||||
bad[118] ^= 0xff;
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
|
||||
|
||||
// A section that is not zlib: corrupt the globals stream's header byte.
|
||||
let globals = u32::from_le_bytes(good[75..79].try_into().unwrap()) as usize;
|
||||
let mut bad = good.clone();
|
||||
bad[globals] = 0;
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Inflate { section: "globals", .. })));
|
||||
|
||||
// Offsets out of order.
|
||||
let mut bad = good;
|
||||
bad[75..79].copy_from_slice(&1u32.to_le_bytes());
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_inflation_ceiling_is_reported_not_exceeded() {
|
||||
// 1,200 inflated bytes and the 3-byte picture.
|
||||
let f = build([&[0u8; 600], &[0u8; 600], b"", b"", b""], b"", 0);
|
||||
assert!(PrcFile::parse_with_limit(&f, 1203).is_ok());
|
||||
assert_eq!(PrcFile::parse_with_limit(&f, 1202).unwrap_err(), PrcError::TooLarge { limit: 1202 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copied_picture_blocks_are_charged_to_the_ceiling() {
|
||||
// Nothing inflates to more than 0 bytes; the header's one picture is 3 bytes.
|
||||
let f = build([b"", b"", b"", b"", b""], b"", 0);
|
||||
assert!(PrcFile::parse_with_limit(&f, 3).is_ok());
|
||||
assert_eq!(PrcFile::parse_with_limit(&f, 2).unwrap_err(), PrcError::TooLarge { limit: 2 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_offset_or_count_past_the_data_is_an_error_not_a_panic() {
|
||||
let mut bad = build(SECTIONS, b"m", 0);
|
||||
// The model file's start (header offset 95) far past the end, its end further still.
|
||||
bad[95..99].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
bad[99..103].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
assert!(PrcFile::parse(&bad).is_err());
|
||||
// A model file ending one byte past the data, its zlib stream whole.
|
||||
let mut bad = build(SECTIONS, b"m", 0);
|
||||
let past = u32::try_from(bad.len() + 1).unwrap();
|
||||
bad[99..103].copy_from_slice(&past.to_le_bytes());
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(_))));
|
||||
// A section count that claims more offsets than the data holds.
|
||||
let mut bad = build(SECTIONS, b"m", 0);
|
||||
bad[67..71].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Truncated(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_file_structures_or_more_than_the_cap_is_refused_by_name() {
|
||||
let good = build(SECTIONS, b"m", 0);
|
||||
// The file-structure count follows the magic, two versions and two ids.
|
||||
assert_eq!(good[43..47], 1u32.to_le_bytes());
|
||||
for n in [0, u32::try_from(MAX_FILE_STRUCTURES).unwrap() + 1] {
|
||||
let mut bad = good.clone();
|
||||
bad[43..47].copy_from_slice(&n.to_le_bytes());
|
||||
assert!(matches!(PrcFile::parse(&bad), Err(PrcError::Malformed(m)) if m == format!("{n} file structures")), "{n}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Arbitrary bytes through every reader: an error or a value, never a panic.
|
||||
|
||||
use pdfcraft_prc::{BitReader, PrcFile, Schema};
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn any_bytes_parse_or_fail_cleanly(data in proptest::collection::vec(any::<u8>(), 0..512), magic in any::<bool>()) {
|
||||
let mut data = data;
|
||||
if magic && data.len() >= 3 {
|
||||
data[..3].copy_from_slice(b"PRC");
|
||||
}
|
||||
let _ = PrcFile::parse_with_limit(&data, 1 << 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_bits_read_or_fail_cleanly(data in proptest::collection::vec(any::<u8>(), 0..64), n in 0u32..40) {
|
||||
let mut r = BitReader::new(&data);
|
||||
let _ = r.unsigned_integer();
|
||||
let _ = r.integer();
|
||||
let _ = r.integer_vbn(n);
|
||||
let _ = r.double_vbn(0.5, n);
|
||||
let _ = r.nbits_then_unsigned();
|
||||
let _ = r.string();
|
||||
let _ = r.float_as_bytes();
|
||||
let _ = r.double();
|
||||
let _ = r.skip_bits(usize::try_from(n).unwrap_or(0));
|
||||
prop_assert!(r.position() <= data.len() * 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_schema_runs_or_fails_cleanly(data in proptest::collection::vec(any::<u8>(), 0..256), entity in 0u32..4) {
|
||||
let mut r = BitReader::new(&data);
|
||||
if let Ok(schema) = Schema::read(&mut r) {
|
||||
let types: Vec<u32> = schema.entity_types().chain([entity]).collect();
|
||||
for t in types {
|
||||
let _ = schema.skip_added_fields(t, &mut r.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2025,2026 Kristof Ralovich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -34,6 +34,7 @@ pub const TABLE: &[(&str, Class)] = &[
|
||||
("filters", Class::Standalone(0)),
|
||||
("crypt", Class::Standalone(0)),
|
||||
("arlington", Class::Standalone(0)),
|
||||
("prc", Class::Standalone(0)),
|
||||
// L1 object layer
|
||||
("cos", Class::Standalone(1)),
|
||||
// L2 core model
|
||||
|
||||
Reference in new issue
Block a user