Skip to main content

brep_render/sheets/
u3d.rs

1//! A U3D (ECMA-363) writer — the 3D stream a PDF `/3D` annotation carries.
2//!
3//! Hand-rolled, no crate, wasm-clean, like the PDF writer beside it. It writes
4//! exactly what a 3D PDF of a part needs and nothing else: MODEL nodes, each
5//! owning either a triangle MESH (a CLOD mesh generator whose base mesh is the
6//! whole mesh — no progressive resolution updates) or a LINE SET, one lit
7//! texture shader and one material per colour, and a shading modifier per node
8//! choosing them. No lights, cameras, textures, bones or animation: the PDF's
9//! own view dictionaries carry the cameras and the lighting scheme.
10//!
11//! # The bit encoder
12//!
13//! Every block payload goes through the U3D bitstream (ECMA-363 §10): a 16-bit
14//! arithmetic coder with static (uniform) and dynamic (adaptive) contexts.
15//! Until the first compressed value, an uncompressed U8 leaves the coder in its
16//! initial state and is emitted as its own eight bits, so a block holding only
17//! uncompressed fields is plain little-endian bytes. [`BitWriter`] follows the
18//! Intel reference implementation (`CIFXBitStreamX`, Apache-2.0) operation for
19//! operation — the order of the renormalisation steps, when a histogram
20//! halves, which contexts a field shares — because the reader on the other end
21//! of a PDF is descended from that code, and an arithmetic coder that is
22//! "equivalent" but not identical decodes to garbage. [`BitReader`] is the
23//! matching decoder, kept for the round-trip tests.
24//!
25//! # The file
26//!
27//! The block order is the reference converter's own for the same content: the
28//! file header, a priority update, a NODE modifier chain per model (the model
29//! node and its shading modifier), a MODEL RESOURCE chain per model (the
30//! generator's declaration, and — for a line set — its continuation), the
31//! shaders, the materials, a second priority update, then each mesh's base-mesh
32//! continuation.
33
34use std::collections::HashMap;
35
36// ---------------------------------------------------------------------------
37// Block types (ECMA-363 §9)
38// ---------------------------------------------------------------------------
39
40const FILE_HEADER: u32 = 0x0044_3355;
41const PRIORITY_UPDATE: u32 = 0xFFFF_FF15;
42const MODIFIER_CHAIN: u32 = 0xFFFF_FF14;
43const MODEL_NODE: u32 = 0xFFFF_FF22;
44const CLOD_MESH_DECLARATION: u32 = 0xFFFF_FF31;
45const CLOD_BASE_MESH_CONTINUATION: u32 = 0xFFFF_FF3B;
46const LINE_SET_DECLARATION: u32 = 0xFFFF_FF37;
47const LINE_SET_CONTINUATION: u32 = 0xFFFF_FF3F;
48const SHADING_MODIFIER: u32 = 0xFFFF_FF45;
49const LIT_TEXTURE_SHADER: u32 = 0xFFFF_FF53;
50const MATERIAL_RESOURCE: u32 = 0xFFFF_FF54;
51
52/// Modifier chain types.
53const CHAIN_NODE: u32 = 0;
54const CHAIN_MODEL_RESOURCE: u32 = 1;
55
56/// The file header's character encoding: UTF-8 (the IANA MIBenum).
57const UTF8: u32 = 106;
58
59// ---------------------------------------------------------------------------
60// Compression contexts — the reference implementation's numbering
61// (`IFXACContext.h`). A context is a histogram; two fields that share a number
62// share a histogram, so these are part of the format, not a local choice.
63// ---------------------------------------------------------------------------
64
65/// The static context of range `r`: every value `0..r` equally likely.
66const STATIC_FULL: u32 = 0x400;
67/// One past the largest static context: a range of `0x3FFF` or more is
68/// written as an uncompressed value instead.
69const MAX_RANGE: u32 = STATIC_FULL + 0x3FFF;
70/// The context an uncompressed U8 is written through once the coder is live.
71const CONTEXT_8: u32 = 0;
72
73const CTX_BASE_SHADING_ID: u32 = 1;
74const CTX_NUM_NEW_FACES: u32 = 1;
75const CTX_LINE_SHADING_ID: u32 = 1;
76const CTX_POSITION_DIFF_SIGNS: u32 = 20;
77const CTX_POSITION_DIFF_MAG_X: u32 = 21;
78const CTX_POSITION_DIFF_MAG_Y: u32 = 22;
79const CTX_POSITION_DIFF_MAG_Z: u32 = 23;
80const CTX_NUM_LOCAL_NORMALS: u32 = 40;
81const CTX_NORMAL_DIFF_SIGNS: u32 = 41;
82const CTX_NORMAL_DIFF_MAG_X: u32 = 42;
83const CTX_NORMAL_DIFF_MAG_Y: u32 = 43;
84const CTX_NORMAL_DIFF_MAG_Z: u32 = 44;
85const CTX_NORMAL_LOCAL_INDEX: u32 = 45;
86
87/// The static context for an index into an array of `count` entries.
88fn static_range(count: u32) -> u32 {
89    STATIC_FULL + count
90}
91
92/// A dynamic histogram halves every count once it has seen this many symbols
93/// (the reference's `m_uElephant`), then gives the escape symbol one back.
94const ELEPHANT: u32 = 0x1FFF;
95/// Symbols above this are never added to a histogram.
96const MAX_SYMBOL_IN_HISTOGRAM: u32 = 0xFFFF;
97
98// ---------------------------------------------------------------------------
99// The scene
100// ---------------------------------------------------------------------------
101
102/// A surface colour: one lit texture shader and one material in the file.
103#[derive(Debug, Clone, PartialEq)]
104pub struct Material {
105    pub name: String,
106    pub ambient: [f32; 3],
107    pub diffuse: [f32; 3],
108    pub specular: [f32; 3],
109    pub emissive: [f32; 3],
110    pub reflectivity: f32,
111    pub opacity: f32,
112}
113
114/// A triangle mesh. `normals[i]` is the normal at `positions[i]`; each
115/// triangle carries the index of the [`Model::shaders`] entry that colours it.
116#[derive(Debug, Clone, Default, PartialEq)]
117pub struct Mesh {
118    pub positions: Vec<[f32; 3]>,
119    pub normals: Vec<[f32; 3]>,
120    pub triangles: Vec<([u32; 3], u32)>,
121}
122
123/// Line segments between `positions`, each with its [`Model::shaders`] index.
124#[derive(Debug, Clone, Default, PartialEq)]
125pub struct Lines {
126    pub positions: Vec<[f32; 3]>,
127    pub segments: Vec<([u32; 2], u32)>,
128}
129
130#[derive(Debug, Clone, PartialEq)]
131pub enum Geometry {
132    Mesh(Mesh),
133    Lines(Lines),
134}
135
136/// One model node and the resource it shows. `name` is the node name a PDF
137/// `/3DNode` dictionary refers to; the resource is named `{name}.res`.
138#[derive(Debug, Clone, PartialEq)]
139pub struct Model {
140    pub name: String,
141    pub geometry: Geometry,
142    /// Material index per shading id (at least one).
143    pub shaders: Vec<usize>,
144    /// Draw back faces too — a lone triangle (an arrowhead) that must not
145    /// vanish when the model is turned round. A closed solid does not need it.
146    pub two_sided: bool,
147}
148
149#[derive(Debug, Clone, Default, PartialEq)]
150pub struct Scene {
151    pub materials: Vec<Material>,
152    pub models: Vec<Model>,
153}
154
155/// Why a scene cannot be written.
156pub fn validate(scene: &Scene) -> Result<(), String> {
157    let mut names = std::collections::HashSet::new();
158    for model in &scene.models {
159        if model.name.is_empty() || model.name.len() > 0xFFFF {
160            return Err(format!("model name '{}' is empty or too long", model.name));
161        }
162        if !names.insert(model.name.as_str()) {
163            return Err(format!("two models are named '{}'", model.name));
164        }
165        if model.shaders.is_empty() {
166            return Err(format!("model '{}' has no shader", model.name));
167        }
168        if let Some(bad) = model.shaders.iter().find(|index| **index >= scene.materials.len()) {
169            return Err(format!("model '{}' names material {bad}, which does not exist", model.name));
170        }
171        let shading = model.shaders.len() as u32;
172        match &model.geometry {
173            Geometry::Mesh(mesh) => {
174                if mesh.positions.is_empty() || mesh.triangles.is_empty() {
175                    return Err(format!("mesh '{}' is empty", model.name));
176                }
177                if mesh.normals.len() != mesh.positions.len() {
178                    return Err(format!("mesh '{}' has a normal count unlike its position count", model.name));
179                }
180                let n = mesh.positions.len() as u32;
181                if mesh.triangles.iter().any(|(t, s)| t.iter().any(|i| *i >= n) || *s >= shading) {
182                    return Err(format!("mesh '{}' indexes past its arrays", model.name));
183                }
184            }
185            Geometry::Lines(lines) => {
186                if lines.positions.is_empty() || lines.segments.is_empty() {
187                    return Err(format!("line set '{}' is empty", model.name));
188                }
189                let n = lines.positions.len() as u32;
190                if lines.segments.iter().any(|(s, m)| s.iter().any(|i| *i >= n) || s[0] == s[1] || *m >= shading) {
191                    return Err(format!("line set '{}' has a degenerate or out-of-range segment", model.name));
192                }
193            }
194        }
195    }
196    for material in &scene.materials {
197        if material.name.is_empty() || material.name.len() > 0xFFFF {
198            return Err(format!("material name '{}' is empty or too long", material.name));
199        }
200    }
201    Ok(())
202}
203
204/// Serialise `scene` as a U3D file.
205pub fn write(scene: &Scene) -> Result<Vec<u8>, String> {
206    validate(scene)?;
207    let mut blocks: Vec<u8> = Vec::new();
208
209    push_block(&mut blocks, PRIORITY_UPDATE, &uncompressed(|w| w.write_u32(0)));
210    for model in &scene.models {
211        let node = block_bytes(MODEL_NODE, &model_node(model));
212        let shading = block_bytes(SHADING_MODIFIER, &shading_modifier(model, scene));
213        push_block(&mut blocks, MODIFIER_CHAIN, &modifier_chain(&model.name, CHAIN_NODE, &[node, shading]));
214    }
215    for model in &scene.models {
216        let resource = resource_name(model);
217        let members = match &model.geometry {
218            Geometry::Mesh(mesh) => vec![block_bytes(CLOD_MESH_DECLARATION, &mesh_declaration(&resource, mesh, model))],
219            Geometry::Lines(lines) => {
220                let quant = line_quantisation(lines);
221                vec![
222                    block_bytes(LINE_SET_DECLARATION, &line_declaration(&resource, lines, model, &quant)),
223                    block_bytes(LINE_SET_CONTINUATION, &line_continuation(&resource, lines, &quant)),
224                ]
225            }
226        };
227        push_block(&mut blocks, MODIFIER_CHAIN, &modifier_chain(&resource, CHAIN_MODEL_RESOURCE, &members));
228    }
229    for material in &scene.materials {
230        push_block(&mut blocks, LIT_TEXTURE_SHADER, &lit_texture_shader(material));
231    }
232    for material in &scene.materials {
233        push_block(&mut blocks, MATERIAL_RESOURCE, &material_resource(material));
234    }
235    push_block(&mut blocks, PRIORITY_UPDATE, &uncompressed(|w| w.write_u32(0x100)));
236    for model in &scene.models {
237        if let Geometry::Mesh(mesh) = &model.geometry {
238            push_block(&mut blocks, CLOD_BASE_MESH_CONTINUATION, &base_mesh(&resource_name(model), mesh));
239        }
240    }
241
242    // The header last, now the file's size is known. Its declaration size is
243    // the header block's own 36 bytes — what the reference writer puts there.
244    const HEADER_BLOCK: u64 = 12 + 24;
245    let file_size = HEADER_BLOCK + blocks.len() as u64;
246    let header = uncompressed(|w| {
247        w.write_u16(0); // major version
248        w.write_u16(0); // minor version
249        w.write_u32(0); // profile: base profile, compression on, no units
250        w.write_u32(HEADER_BLOCK as u32);
251        w.write_u64(file_size);
252        w.write_u32(UTF8);
253    });
254    let mut out = Vec::with_capacity(file_size as usize);
255    push_block(&mut out, FILE_HEADER, &header);
256    out.extend_from_slice(&blocks);
257    Ok(out)
258}
259
260fn resource_name(model: &Model) -> String {
261    format!("{}.res", model.name)
262}
263
264/// A block: type, data size, metadata size (always 0 here), the data padded
265/// to four bytes.
266fn push_block(out: &mut Vec<u8>, block_type: u32, data: &[u8]) {
267    out.extend_from_slice(&block_type.to_le_bytes());
268    out.extend_from_slice(&(data.len() as u32).to_le_bytes());
269    out.extend_from_slice(&0u32.to_le_bytes());
270    out.extend_from_slice(data);
271    out.resize(out.len() + (4 - data.len() % 4) % 4, 0);
272}
273
274fn block_bytes(block_type: u32, data: &[u8]) -> Vec<u8> {
275    let mut out = Vec::new();
276    push_block(&mut out, block_type, data);
277    out
278}
279
280fn uncompressed(body: impl FnOnce(&mut BitWriter)) -> Vec<u8> {
281    let mut w = BitWriter::default();
282    body(&mut w);
283    w.finish()
284}
285
286/// A modifier chain holding `members` (each already a whole block). The
287/// member count sits on a four-byte boundary of the chain's data.
288fn modifier_chain(name: &str, chain_type: u32, members: &[Vec<u8>]) -> Vec<u8> {
289    let mut head = uncompressed(|w| {
290        w.write_string(name);
291        w.write_u32(chain_type);
292        w.write_u32(0); // attributes: no bounding sphere, no bounding box
293    });
294    head.resize(head.len() + (4 - head.len() % 4) % 4, 0);
295    head.extend_from_slice(&(members.len() as u32).to_le_bytes());
296    for member in members {
297        head.extend_from_slice(member);
298    }
299    head
300}
301
302const IDENTITY: [f32; 16] = [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];
303
304fn model_node(model: &Model) -> Vec<u8> {
305    uncompressed(|w| {
306        w.write_string(&model.name);
307        w.write_u32(1); // one parent: the world
308        w.write_string("");
309        for value in IDENTITY {
310            w.write_f32(value);
311        }
312        w.write_string(&resource_name(model));
313        // Visibility: front faces (the reference's value), or front and back.
314        w.write_u32(if model.two_sided { 3 } else { 1 });
315    })
316}
317
318fn shading_modifier(model: &Model, scene: &Scene) -> Vec<u8> {
319    uncompressed(|w| {
320        w.write_string(&model.name);
321        w.write_u32(1); // chain index: after the model node
322        w.write_u32(0x0F); // applies to meshes, lines, points and glyphs
323        w.write_u32(model.shaders.len() as u32);
324        for material in &model.shaders {
325            w.write_u32(1); // one shader in this shader list
326            w.write_string(&scene.materials[*material].name);
327        }
328    })
329}
330
331/// The shading descriptions: every shader list uses positions and normals
332/// only — no vertex colours, no texture layers.
333fn write_shading_descriptions(w: &mut BitWriter, count: usize) {
334    w.write_u32(count as u32);
335    for index in 0..count {
336        w.write_u32(0); // attributes: no per-vertex colours
337        w.write_u32(0); // texture layer count
338        w.write_u32(index as u32); // original shading id
339    }
340}
341
342fn mesh_declaration(resource: &str, mesh: &Mesh, model: &Model) -> Vec<u8> {
343    uncompressed(|w| {
344        w.write_string(resource);
345        w.write_u32(0); // chain index
346        w.write_u32(0); // mesh attributes: normals present
347        w.write_u32(mesh.triangles.len() as u32);
348        w.write_u32(mesh.positions.len() as u32);
349        w.write_u32(mesh.normals.len() as u32);
350        w.write_u32(0); // diffuse colours
351        w.write_u32(0); // specular colours
352        w.write_u32(0); // texture coordinates
353        write_shading_descriptions(w, model.shaders.len());
354        // CLOD: the base mesh is the whole mesh.
355        w.write_u32(mesh.positions.len() as u32);
356        w.write_u32(mesh.positions.len() as u32);
357        // Quality factors (informative) and inverse quantisation: the base
358        // mesh is not quantised, so these are the reference's defaults.
359        for _ in 0..3 {
360            w.write_u32(1000);
361        }
362        for _ in 0..5 {
363            w.write_f32(1.0);
364        }
365        // Normal crease, update and tolerance: merge nothing.
366        w.write_f32(-1.0);
367        w.write_f32(0.0);
368        w.write_f32(0.0);
369        w.write_u32(0); // bones
370    })
371}
372
373fn base_mesh(resource: &str, mesh: &Mesh) -> Vec<u8> {
374    let mut w = BitWriter::default();
375    w.write_string(resource);
376    w.write_u32(0); // chain index
377    w.write_u32(mesh.triangles.len() as u32);
378    w.write_u32(mesh.positions.len() as u32);
379    w.write_u32(mesh.normals.len() as u32);
380    w.write_u32(0);
381    w.write_u32(0);
382    w.write_u32(0);
383    for p in &mesh.positions {
384        for c in p {
385            w.write_f32(*c);
386        }
387    }
388    for n in &mesh.normals {
389        for c in n {
390            w.write_f32(*c);
391        }
392    }
393    let positions = static_range(mesh.positions.len() as u32);
394    let normals = static_range(mesh.normals.len() as u32);
395    for (corners, shading) in &mesh.triangles {
396        w.write_compressed_u32(CTX_BASE_SHADING_ID, *shading);
397        for corner in corners {
398            w.write_compressed_u32(positions, *corner);
399            w.write_compressed_u32(normals, *corner);
400        }
401    }
402    w.finish()
403}
404
405/// A line set's position quantisation (the reference's rule at quality
406/// 1000): `2^18` steps across the bounding radius, capped so no coordinate
407/// overflows a U32.
408struct LineQuant {
409    position: f32,
410    normal: f32,
411}
412
413fn line_quantisation(lines: &Lines) -> LineQuant {
414    let mut min = lines.positions[0];
415    let mut max = lines.positions[0];
416    for p in &lines.positions {
417        for axis in 0..3 {
418            min[axis] = min[axis].min(p[axis]);
419            max[axis] = max[axis].max(p[axis]);
420        }
421    }
422    let centre = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5, (min[2] + max[2]) * 0.5];
423    let radius_squared = lines
424        .positions
425        .iter()
426        .map(|p| (p[0] - centre[0]).powi(2) + (p[1] - centre[1]).powi(2) + (p[2] - centre[2]).powi(2))
427        .fold(0.0f32, f32::max);
428    let mut position = 262_144.0f32;
429    if radius_squared > 0.0 {
430        position /= radius_squared.sqrt();
431    }
432    let largest = min.iter().chain(max.iter()).map(|c| c.abs()).fold(0.0f32, f32::max);
433    if largest > 0.0 {
434        position = position.min(0xFFFF_FE00u32 as f32 / largest);
435    }
436    LineQuant { position, normal: 16_384.0 }
437}
438
439fn line_declaration(resource: &str, lines: &Lines, model: &Model, quant: &LineQuant) -> Vec<u8> {
440    uncompressed(|w| {
441        w.write_string(resource);
442        w.write_u32(0); // chain index
443        w.write_u32(0); // reserved
444        w.write_u32(lines.segments.len() as u32);
445        w.write_u32(lines.positions.len() as u32);
446        w.write_u32(1); // one normal, shared by every line end
447        w.write_u32(0);
448        w.write_u32(0);
449        w.write_u32(0);
450        write_shading_descriptions(w, model.shaders.len());
451        for _ in 0..3 {
452            w.write_u32(0); // quality factors, as the reference writes them
453        }
454        w.write_f32(1.0 / quant.position);
455        w.write_f32(1.0 / quant.normal);
456        w.write_f32(1.0 / 16_384.0); // texture coordinates
457        w.write_f32(1.0 / 16_384.0); // diffuse colours
458        w.write_f32(1.0 / 16_384.0); // specular colours
459        for _ in 0..3 {
460            w.write_u32(0); // reserved line set parameters
461        }
462        w.write_u32(0); // bones
463    })
464}
465
466/// The single line-end normal: every line end uses it, so every reconstructed
467/// normal is exactly it and the predicted normal is either it or zero.
468const LINE_NORMAL: [f32; 3] = [0.0, 0.0, 1.0];
469
470/// Signs and magnitudes of a quantised difference (ECMA-363 §5.3.3).
471fn quantise(v: [f32; 3], factor: f32) -> (u8, [u32; 3]) {
472    let signs = (v[0] < 0.0) as u8 | ((v[1] < 0.0) as u8) << 1 | ((v[2] < 0.0) as u8) << 2;
473    let mag = |c: f32| (0.5f32 + factor * c.abs()) as u32;
474    (signs, [mag(v[0]), mag(v[1]), mag(v[2])])
475}
476
477fn reconstruct(predicted: [f32; 3], signs: u8, mag: [u32; 3], inverse: f32) -> [f32; 3] {
478    let mut out = predicted;
479    for axis in 0..3 {
480        let step = inverse * mag[axis] as f32;
481        out[axis] = if signs & (1 << axis) != 0 { predicted[axis] - step } else { predicted[axis] + step };
482    }
483    out
484}
485
486/// The line set's data, one block (a block of 4096 positions or more would be
487/// split by the reference; one block reads the same and a PMI set is far
488/// smaller).
489///
490/// Each position `i` is written as a difference from position `i − 1` (the
491/// SPLIT position) and brings every segment whose other end is an earlier
492/// position. The prediction is the RECONSTRUCTED previous position — what the
493/// reader has — so quantisation error does not accumulate along the set.
494fn line_continuation(resource: &str, lines: &Lines, quant: &LineQuant) -> Vec<u8> {
495    let count = lines.positions.len() as u32;
496    // Segments by their LATER end, with the earlier end: that is when each is
497    // written.
498    let mut by_later: Vec<Vec<(u32, u32)>> = vec![Vec::new(); lines.positions.len()];
499    for ([a, b], shading) in &lines.segments {
500        let (early, late) = if a < b { (*a, *b) } else { (*b, *a) };
501        by_later[late as usize].push((early, *shading));
502    }
503    // Which positions have a segment already written — the predicted normal
504    // at a split position is the line normal once one exists, else zero.
505    let mut has_line = vec![false; lines.positions.len()];
506
507    let mut w = BitWriter::default();
508    w.write_string(resource);
509    w.write_u32(0); // chain index
510    w.write_u32(0); // start resolution
511    w.write_u32(count); // end resolution
512    let inverse = 1.0 / quant.position;
513    let mut previous = [0.0f32; 3];
514    for current in 0..count {
515        let position = lines.positions[current as usize];
516        let (split_range, split, predicted) = if current == 0 {
517            (static_range(1), 0, [0.0f32; 3])
518        } else {
519            (static_range(current), current - 1, previous)
520        };
521        w.write_compressed_u32(split_range, split);
522        let diff = [position[0] - predicted[0], position[1] - predicted[1], position[2] - predicted[2]];
523        let (signs, mag) = quantise(diff, quant.position);
524        w.write_compressed_u8(CTX_POSITION_DIFF_SIGNS, signs);
525        w.write_compressed_u32(CTX_POSITION_DIFF_MAG_X, mag[0]);
526        w.write_compressed_u32(CTX_POSITION_DIFF_MAG_Y, mag[1]);
527        w.write_compressed_u32(CTX_POSITION_DIFF_MAG_Z, mag[2]);
528        previous = reconstruct(predicted, signs, mag, inverse);
529        if current == 0 {
530            w.write_compressed_u32(CTX_NUM_LOCAL_NORMALS, 0);
531            w.write_compressed_u32(CTX_NUM_NEW_FACES, 0);
532            continue;
533        }
534        let new_lines = &by_later[current as usize];
535        let predicted_normal = if has_line[split as usize] { LINE_NORMAL } else { [0.0; 3] };
536        w.write_compressed_u32(CTX_NUM_LOCAL_NORMALS, 2 * new_lines.len() as u32);
537        let normal_diff = [
538            LINE_NORMAL[0] - predicted_normal[0],
539            LINE_NORMAL[1] - predicted_normal[1],
540            LINE_NORMAL[2] - predicted_normal[2],
541        ];
542        let (normal_signs, normal_mag) = quantise(normal_diff, quant.normal);
543        for _ in 0..new_lines.len() * 2 {
544            w.write_compressed_u8(CTX_NORMAL_DIFF_SIGNS, normal_signs);
545            w.write_compressed_u32(CTX_NORMAL_DIFF_MAG_X, normal_mag[0]);
546            w.write_compressed_u32(CTX_NORMAL_DIFF_MAG_Y, normal_mag[1]);
547            w.write_compressed_u32(CTX_NORMAL_DIFF_MAG_Z, normal_mag[2]);
548        }
549        w.write_compressed_u32(CTX_NUM_NEW_FACES, new_lines.len() as u32);
550        for (index, (early, shading)) in new_lines.iter().enumerate() {
551            w.write_compressed_u32(CTX_LINE_SHADING_ID, *shading);
552            w.write_compressed_u32(static_range(current), *early);
553            for end in 0..2 {
554                w.write_compressed_u32(CTX_NORMAL_LOCAL_INDEX, 2 * index as u32 + end);
555            }
556            has_line[*early as usize] = true;
557        }
558        if !new_lines.is_empty() {
559            has_line[current as usize] = true;
560        }
561    }
562    w.finish()
563}
564
565fn lit_texture_shader(material: &Material) -> Vec<u8> {
566    uncompressed(|w| {
567        w.write_string(&material.name);
568        w.write_u32(1); // lighting enabled
569        w.write_f32(0.0); // alpha test reference
570        w.write_u32(0x617); // alpha test: always
571        w.write_u32(0x606); // colour blend: alpha blend
572        w.write_u32(1); // render pass 0 enabled
573        w.write_u32(0); // no texture channels
574        w.write_u32(0); // no alpha texture channels
575        w.write_string(&material.name);
576    })
577}
578
579fn material_resource(material: &Material) -> Vec<u8> {
580    uncompressed(|w| {
581        w.write_string(&material.name);
582        w.write_u32(0x3F); // every colour, reflectivity and opacity present
583        for colour in [material.ambient, material.diffuse, material.specular, material.emissive] {
584            for c in colour {
585                w.write_f32(c);
586            }
587        }
588        w.write_f32(material.reflectivity);
589        w.write_f32(material.opacity);
590    })
591}
592
593// ---------------------------------------------------------------------------
594// The bitstream
595// ---------------------------------------------------------------------------
596
597/// A dynamic histogram: symbol 0 is the escape, starting at frequency 1.
598/// Cumulative frequencies come from a Fenwick tree, because a magnitude
599/// context can hold symbols up to `0xFFFF` and a linear scan per symbol would
600/// make a large line set quadratic.
601#[derive(Debug, Clone)]
602struct Histogram {
603    counts: Vec<u32>,
604    tree: Vec<u32>,
605    total: u32,
606}
607
608impl Histogram {
609    fn new() -> Self {
610        let mut h = Histogram { counts: vec![0; 128], tree: vec![0; 129], total: 0 };
611        h.bump(0, 1);
612        h
613    }
614
615    fn bump(&mut self, symbol: usize, by: u32) {
616        if symbol >= self.counts.len() {
617            let mut size = self.counts.len();
618            while size <= symbol {
619                size *= 2;
620            }
621            let counts = std::mem::take(&mut self.counts);
622            self.counts = vec![0; size];
623            self.tree = vec![0; size + 1];
624            self.total = 0;
625            for (s, c) in counts.into_iter().enumerate() {
626                if c > 0 {
627                    self.bump(s, c);
628                }
629            }
630        }
631        self.counts[symbol] += by;
632        self.total += by;
633        let mut i = symbol + 1;
634        while i < self.tree.len() {
635            self.tree[i] += by;
636            i += i & i.wrapping_neg();
637        }
638    }
639
640    fn freq(&self, symbol: u32) -> u32 {
641        self.counts.get(symbol as usize).copied().unwrap_or(0)
642    }
643
644    /// The summed frequency of every symbol below `symbol`.
645    fn cum(&self, symbol: u32) -> u32 {
646        let mut i = (symbol as usize).min(self.counts.len());
647        let mut sum = 0;
648        while i > 0 {
649            sum += self.tree[i];
650            i -= i & i.wrapping_neg();
651        }
652        sum
653    }
654
655    /// The symbol whose cumulative interval holds `target`.
656    fn symbol_at(&self, target: u32) -> u32 {
657        // The largest prefix whose sum is ≤ target.
658        let mut position = 0usize;
659        let mut remaining = target;
660        let mut step = self.tree.len().next_power_of_two() / 2;
661        while step > 0 {
662            let next = position + step;
663            if next < self.tree.len() && self.tree[next] <= remaining {
664                position = next;
665                remaining -= self.tree[next];
666            }
667            step /= 2;
668        }
669        position as u32
670    }
671
672    fn add(&mut self, symbol: u32) {
673        if symbol > MAX_SYMBOL_IN_HISTOGRAM {
674            return;
675        }
676        if self.total >= ELEPHANT {
677            let halved: Vec<u32> = self.counts.iter().map(|c| c >> 1).collect();
678            self.tree.iter_mut().for_each(|t| *t = 0);
679            self.counts.iter_mut().for_each(|c| *c = 0);
680            self.total = 0;
681            for (s, c) in halved.into_iter().enumerate() {
682                if c > 0 {
683                    self.bump(s, c);
684                }
685            }
686            self.bump(0, 1);
687        }
688        self.bump(symbol as usize, 1);
689    }
690}
691
692const HALF: u32 = 0x8000;
693const QUARTER: u32 = 0x4000;
694
695/// The U3D bit writer (see the module doc).
696#[derive(Debug, Clone)]
697struct BitWriter {
698    bytes: Vec<u8>,
699    bit: usize,
700    high: u32,
701    low: u32,
702    underflow: u32,
703    compressed: bool,
704    contexts: HashMap<u32, Histogram>,
705}
706
707impl Default for BitWriter {
708    fn default() -> Self {
709        BitWriter {
710            bytes: Vec::new(),
711            bit: 0,
712            high: 0xFFFF,
713            low: 0,
714            underflow: 0,
715            compressed: false,
716            contexts: HashMap::new(),
717        }
718    }
719}
720
721/// Reverse the bit order of a byte.
722fn swap8(value: u32) -> u32 {
723    (value as u8).reverse_bits() as u32
724}
725
726impl BitWriter {
727    fn write_bit(&mut self, value: u32) {
728        let byte = self.bit / 8;
729        if byte == self.bytes.len() {
730            self.bytes.push(0);
731        }
732        if value & 1 != 0 {
733            self.bytes[byte] |= 1 << (self.bit % 8);
734        }
735        self.bit += 1;
736    }
737
738    fn initial(&self) -> bool {
739        self.high == 0xFFFF && self.low == 0 && self.underflow == 0
740    }
741
742    fn write_u8(&mut self, value: u8) {
743        let symbol = swap8(value as u32);
744        if self.initial() {
745            for i in 0..8 {
746                self.write_bit((value as u32) >> i);
747            }
748        } else {
749            self.write_static(STATIC_FULL + 256, symbol + 1);
750        }
751    }
752
753    fn write_u16(&mut self, value: u16) {
754        self.write_u8(value as u8);
755        self.write_u8((value >> 8) as u8);
756    }
757
758    fn write_u32(&mut self, value: u32) {
759        self.write_u16(value as u16);
760        self.write_u16((value >> 16) as u16);
761    }
762
763    fn write_u64(&mut self, value: u64) {
764        self.write_u32(value as u32);
765        self.write_u32((value >> 32) as u32);
766    }
767
768    fn write_f32(&mut self, value: f32) {
769        self.write_u32(value.to_bits());
770    }
771
772    fn write_string(&mut self, text: &str) {
773        self.write_u16(text.len() as u16);
774        for byte in text.bytes() {
775            self.write_u8(byte);
776        }
777    }
778
779    /// Narrow the interval to `[cum, cum + freq)` of `total`, then emit every
780    /// settled bit — the reference's renormalisation, step for step.
781    fn encode(&mut self, cum: u32, freq: u32, total: u32) {
782        let range = self.high + 1 - self.low;
783        // `low - 1` wraps when low is 0, exactly as the reference's U32 does;
784        // the sum lands back in range.
785        self.high = self.low.wrapping_sub(1).wrapping_add(range * (cum + freq) / total);
786        self.low += range * cum / total;
787        let mut bit = self.low >> 15;
788        while (self.high & HALF) == (self.low & HALF) {
789            self.high &= !HALF;
790            self.high += self.high + 1;
791            self.write_bit(bit);
792            while self.underflow > 0 {
793                self.underflow -= 1;
794                self.write_bit(!bit & 1);
795            }
796            self.low &= !HALF;
797            self.low += self.low;
798            bit = self.low >> 15;
799        }
800        while (self.high & QUARTER) == 0 && (self.low & QUARTER) == QUARTER {
801            self.high &= !HALF;
802            self.high <<= 1;
803            self.low <<= 1;
804            self.high |= HALF;
805            self.high |= 1;
806            self.low &= !HALF;
807            self.underflow += 1;
808        }
809    }
810
811    /// A symbol `1..=R` in the static context `STATIC_FULL + R`.
812    fn write_static(&mut self, context: u32, symbol: u32) {
813        let total = context - STATIC_FULL;
814        debug_assert!(symbol >= 1 && symbol <= total);
815        self.encode(symbol - 1, 1, total);
816    }
817
818    /// A symbol in a dynamic context; true when the escape was written
819    /// instead.
820    fn write_dynamic(&mut self, context: u32, symbol: u32) -> bool {
821        let histogram = self.contexts.entry(context).or_insert_with(Histogram::new);
822        let (mut symbol, total) = (symbol, histogram.total);
823        let mut freq = histogram.freq(symbol);
824        if freq == 0 {
825            symbol = 0;
826            freq = histogram.freq(0);
827        }
828        let cum = histogram.cum(symbol);
829        histogram.add(symbol);
830        self.encode(cum, freq, total);
831        symbol == 0
832    }
833
834    fn write_compressed(&mut self, context: u32, value: u32, raw: impl Fn(&mut Self, u32)) {
835        self.compressed = true;
836        if context != CONTEXT_8 && context < MAX_RANGE {
837            let escaped = if context > STATIC_FULL {
838                self.write_static(context, value + 1);
839                false
840            } else {
841                self.write_dynamic(context, value + 1)
842            };
843            if escaped {
844                raw(self, value);
845                if let Some(histogram) = self.contexts.get_mut(&context) {
846                    histogram.add(value + 1);
847                }
848            }
849        } else {
850            raw(self, value);
851        }
852    }
853
854    fn write_compressed_u32(&mut self, context: u32, value: u32) {
855        self.write_compressed(context, value, |w, v| w.write_u32(v));
856    }
857
858    fn write_compressed_u8(&mut self, context: u32, value: u8) {
859        self.write_compressed(context, value as u32, |w, v| w.write_u8(v as u8));
860    }
861
862    /// The block's bytes: a compressed block is flushed with an uncompressed
863    /// U32 zero so every bit the reader needs is written.
864    fn finish(mut self) -> Vec<u8> {
865        if self.compressed {
866            self.write_u32(0);
867        }
868        self.bytes.truncate(self.bit.div_ceil(8));
869        self.bytes
870    }
871}
872
873
874