#![allow(clippy::upper_case_acronyms)]
use crate::annotation_types::AnnotationFlags;
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ThreeDFormat {
#[default]
U3D,
PRC,
}
impl ThreeDFormat {
pub fn subtype(&self) -> &'static str {
match self {
ThreeDFormat::U3D => "U3D",
ThreeDFormat::PRC => "PRC",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ThreeDLighting {
Artwork,
None,
#[default]
White,
Day,
Night,
Hard,
Primary,
Blue,
Red,
Cube,
CAD,
Headlamp,
}
impl ThreeDLighting {
pub fn pdf_name(&self) -> &'static str {
match self {
ThreeDLighting::Artwork => "Artwork",
ThreeDLighting::None => "None",
ThreeDLighting::White => "White",
ThreeDLighting::Day => "Day",
ThreeDLighting::Night => "Night",
ThreeDLighting::Hard => "Hard",
ThreeDLighting::Primary => "Primary",
ThreeDLighting::Blue => "Blue",
ThreeDLighting::Red => "Red",
ThreeDLighting::Cube => "Cube",
ThreeDLighting::CAD => "CAD",
ThreeDLighting::Headlamp => "Headlamp",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ThreeDRenderMode {
#[default]
Solid,
SolidWireframe,
Transparent,
TransparentWireframe,
BoundingBox,
TransparentBoundingBox,
TransparentBoundingBoxOutline,
Wireframe,
ShadedWireframe,
HiddenWireframe,
Vertices,
ShadedVertices,
Illustration,
SolidOutline,
ShadedIllustration,
}
impl ThreeDRenderMode {
pub fn pdf_name(&self) -> &'static str {
match self {
ThreeDRenderMode::Solid => "Solid",
ThreeDRenderMode::SolidWireframe => "SolidWireframe",
ThreeDRenderMode::Transparent => "Transparent",
ThreeDRenderMode::TransparentWireframe => "TransparentWireframe",
ThreeDRenderMode::BoundingBox => "BoundingBox",
ThreeDRenderMode::TransparentBoundingBox => "TransparentBoundingBox",
ThreeDRenderMode::TransparentBoundingBoxOutline => "TransparentBoundingBoxOutline",
ThreeDRenderMode::Wireframe => "Wireframe",
ThreeDRenderMode::ShadedWireframe => "ShadedWireframe",
ThreeDRenderMode::HiddenWireframe => "HiddenWireframe",
ThreeDRenderMode::Vertices => "Vertices",
ThreeDRenderMode::ShadedVertices => "ShadedVertices",
ThreeDRenderMode::Illustration => "Illustration",
ThreeDRenderMode::SolidOutline => "SolidOutline",
ThreeDRenderMode::ShadedIllustration => "ShadedIllustration",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ThreeDProjection {
Orthographic,
#[default]
Perspective,
}
impl ThreeDProjection {
pub fn pdf_name(&self) -> &'static str {
match self {
ThreeDProjection::Orthographic => "Orthographic",
ThreeDProjection::Perspective => "Perspective",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ThreeDActivation {
#[default]
PageVisible,
PageOpen,
Explicit,
}
impl ThreeDActivation {
pub fn pdf_name(&self) -> &'static str {
match self {
ThreeDActivation::PageVisible => "PV",
ThreeDActivation::PageOpen => "PO",
ThreeDActivation::Explicit => "XA",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ThreeDDeactivation {
#[default]
PageNotVisible,
PageClose,
Explicit,
}
impl ThreeDDeactivation {
pub fn pdf_name(&self) -> &'static str {
match self {
ThreeDDeactivation::PageNotVisible => "PI",
ThreeDDeactivation::PageClose => "PC",
ThreeDDeactivation::Explicit => "XD",
}
}
}
#[derive(Debug, Clone)]
pub struct ThreeDBackground {
pub color: (f32, f32, f32),
}
impl Default for ThreeDBackground {
fn default() -> Self {
Self {
color: (1.0, 1.0, 1.0), }
}
}
impl ThreeDBackground {
pub fn new(r: f32, g: f32, b: f32) -> Self {
Self {
color: (r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)),
}
}
pub fn white() -> Self {
Self::default()
}
pub fn black() -> Self {
Self {
color: (0.0, 0.0, 0.0),
}
}
pub fn gray(level: f32) -> Self {
let l = level.clamp(0.0, 1.0);
Self { color: (l, l, l) }
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("3DBG".to_string()));
dict.insert("CS".to_string(), Object::Name("DeviceRGB".to_string()));
dict.insert(
"C".to_string(),
Object::Array(vec![
Object::Real(self.color.0 as f64),
Object::Real(self.color.1 as f64),
Object::Real(self.color.2 as f64),
]),
);
dict
}
}
#[derive(Debug, Clone)]
pub struct ThreeDCamera {
pub distance: f32,
pub azimuth: f32,
pub elevation: f32,
pub center: (f32, f32, f32),
pub fov: f32,
pub roll: f32,
}
impl Default for ThreeDCamera {
fn default() -> Self {
Self {
distance: 100.0,
azimuth: 45.0,
elevation: 30.0,
center: (0.0, 0.0, 0.0),
fov: 45.0,
roll: 0.0,
}
}
}
impl ThreeDCamera {
pub fn new() -> Self {
Self::default()
}
pub fn with_distance(mut self, distance: f32) -> Self {
self.distance = distance;
self
}
pub fn with_azimuth(mut self, degrees: f32) -> Self {
self.azimuth = degrees;
self
}
pub fn with_elevation(mut self, degrees: f32) -> Self {
self.elevation = degrees;
self
}
pub fn with_center(mut self, x: f32, y: f32, z: f32) -> Self {
self.center = (x, y, z);
self
}
pub fn with_fov(mut self, degrees: f32) -> Self {
self.fov = degrees;
self
}
pub fn with_roll(mut self, degrees: f32) -> Self {
self.roll = degrees;
self
}
pub fn calculate_matrix(&self) -> Vec<f64> {
let az = self.azimuth.to_radians();
let el = self.elevation.to_radians();
let rl = self.roll.to_radians();
let cos_az = az.cos();
let sin_az = az.sin();
let cos_el = el.cos();
let sin_el = el.sin();
let cos_rl = rl.cos();
let sin_rl = rl.sin();
let cam_x = self.distance * cos_el * sin_az + self.center.0;
let cam_y = self.distance * cos_el * cos_az + self.center.1;
let cam_z = self.distance * sin_el + self.center.2;
let look_x = self.center.0 - cam_x;
let look_y = self.center.1 - cam_y;
let look_z = self.center.2 - cam_z;
let look_len = (look_x * look_x + look_y * look_y + look_z * look_z).sqrt();
let zx = -look_x / look_len;
let zy = -look_y / look_len;
let zz = -look_z / look_len;
let up_x = 0.0;
let up_y = 0.0;
let up_z = 1.0;
let rx = up_y * zz - up_z * zy;
let ry = up_z * zx - up_x * zz;
let rz = up_x * zy - up_y * zx;
let right_len = (rx * rx + ry * ry + rz * rz).sqrt();
let xx = rx / right_len;
let xy = ry / right_len;
let xz = rz / right_len;
let ux = zy * xz - zz * xy;
let uy = zz * xx - zx * xz;
let uz = zx * xy - zy * xx;
let yx = ux * cos_rl - xx * sin_rl;
let yy = uy * cos_rl - xy * sin_rl;
let yz = uz * cos_rl - xz * sin_rl;
let xx_r = xx * cos_rl + ux * sin_rl;
let xy_r = xy * cos_rl + uy * sin_rl;
let xz_r = xz * cos_rl + uz * sin_rl;
vec![
xx_r as f64,
xy_r as f64,
xz_r as f64,
yx as f64,
yy as f64,
yz as f64,
zx as f64,
zy as f64,
zz as f64,
cam_x as f64,
cam_y as f64,
cam_z as f64,
]
}
}
#[derive(Debug, Clone, Default)]
pub struct ThreeDView {
pub name: Option<String>,
pub camera: ThreeDCamera,
pub projection: ThreeDProjection,
pub render_mode: ThreeDRenderMode,
pub lighting: ThreeDLighting,
pub background: Option<ThreeDBackground>,
}
impl ThreeDView {
pub fn new() -> Self {
Self::default()
}
pub fn default_view() -> Self {
Self {
name: Some("Default".to_string()),
camera: ThreeDCamera::default()
.with_azimuth(45.0)
.with_elevation(30.0),
projection: ThreeDProjection::Perspective,
render_mode: ThreeDRenderMode::Solid,
lighting: ThreeDLighting::White,
background: None,
}
}
pub fn front() -> Self {
Self {
name: Some("Front".to_string()),
camera: ThreeDCamera::default()
.with_azimuth(0.0)
.with_elevation(0.0),
..Default::default()
}
}
pub fn top() -> Self {
Self {
name: Some("Top".to_string()),
camera: ThreeDCamera::default()
.with_azimuth(0.0)
.with_elevation(90.0),
..Default::default()
}
}
pub fn right() -> Self {
Self {
name: Some("Right".to_string()),
camera: ThreeDCamera::default()
.with_azimuth(90.0)
.with_elevation(0.0),
..Default::default()
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_camera_distance(mut self, distance: f32) -> Self {
self.camera.distance = distance;
self
}
pub fn with_camera(mut self, camera: ThreeDCamera) -> Self {
self.camera = camera;
self
}
pub fn with_projection(mut self, projection: ThreeDProjection) -> Self {
self.projection = projection;
self
}
pub fn with_render_mode(mut self, mode: ThreeDRenderMode) -> Self {
self.render_mode = mode;
self
}
pub fn with_lighting(mut self, lighting: ThreeDLighting) -> Self {
self.lighting = lighting;
self
}
pub fn with_background(mut self, background: ThreeDBackground) -> Self {
self.background = Some(background);
self
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("3DView".to_string()));
if let Some(ref name) = self.name {
dict.insert("XN".to_string(), Object::String(name.as_bytes().to_vec()));
} else {
dict.insert("XN".to_string(), Object::String("Default".as_bytes().to_vec()));
}
if let Some(ref name) = self.name {
dict.insert("IN".to_string(), Object::String(name.as_bytes().to_vec()));
}
let matrix = self.camera.calculate_matrix();
dict.insert(
"C2W".to_string(),
Object::Array(matrix.into_iter().map(Object::Real).collect()),
);
dict.insert("CO".to_string(), Object::Real(self.camera.distance as f64));
let mut proj = HashMap::new();
proj.insert("Subtype".to_string(), Object::Name(self.projection.pdf_name().to_string()));
if matches!(self.projection, ThreeDProjection::Perspective) {
proj.insert("FOV".to_string(), Object::Real(self.camera.fov as f64));
}
dict.insert("P".to_string(), Object::Dictionary(proj));
let mut rm = HashMap::new();
rm.insert("Type".to_string(), Object::Name("3DRenderMode".to_string()));
rm.insert("Subtype".to_string(), Object::Name(self.render_mode.pdf_name().to_string()));
dict.insert("RM".to_string(), Object::Dictionary(rm));
let mut ls = HashMap::new();
ls.insert("Type".to_string(), Object::Name("3DLightingScheme".to_string()));
ls.insert("Subtype".to_string(), Object::Name(self.lighting.pdf_name().to_string()));
dict.insert("LS".to_string(), Object::Dictionary(ls));
if let Some(ref bg) = self.background {
dict.insert("BG".to_string(), Object::Dictionary(bg.build()));
}
dict
}
}
#[derive(Debug, Clone)]
pub struct ThreeDStream {
pub data: Vec<u8>,
pub format: ThreeDFormat,
pub animation_style: Option<String>,
}
impl ThreeDStream {
pub fn u3d(data: Vec<u8>) -> Self {
Self {
data,
format: ThreeDFormat::U3D,
animation_style: None,
}
}
pub fn prc(data: Vec<u8>) -> Self {
Self {
data,
format: ThreeDFormat::PRC,
animation_style: None,
}
}
pub fn new(data: Vec<u8>, format: ThreeDFormat) -> Self {
Self {
data,
format,
animation_style: None,
}
}
pub fn with_animation_style(mut self, style: impl Into<String>) -> Self {
self.animation_style = Some(style.into());
self
}
pub fn build(&self) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("3D".to_string()));
dict.insert("Subtype".to_string(), Object::Name(self.format.subtype().to_string()));
if let Some(ref anim) = self.animation_style {
let mut anim_dict = HashMap::new();
anim_dict.insert("Subtype".to_string(), Object::Name(anim.clone()));
dict.insert("AN".to_string(), Object::Dictionary(anim_dict));
}
dict
}
pub fn data(&self) -> &[u8] {
&self.data
}
}
#[derive(Debug, Clone)]
pub struct ThreeDAnnotation {
pub rect: Rect,
pub stream: ThreeDStream,
pub default_view: ThreeDView,
pub views: Vec<ThreeDView>,
pub activation: ThreeDActivation,
pub deactivation: ThreeDDeactivation,
pub title: Option<String>,
pub flags: AnnotationFlags,
pub contents: Option<String>,
pub interactive: bool,
pub toolbar: bool,
pub nav_panel: bool,
}
impl ThreeDAnnotation {
pub fn new(rect: Rect, stream: ThreeDStream) -> Self {
Self {
rect,
stream,
default_view: ThreeDView::default_view(),
views: Vec::new(),
activation: ThreeDActivation::default(),
deactivation: ThreeDDeactivation::default(),
title: None,
flags: AnnotationFlags::printable(),
contents: None,
interactive: true,
toolbar: true,
nav_panel: false,
}
}
pub fn u3d(rect: Rect, data: Vec<u8>) -> Self {
Self::new(rect, ThreeDStream::u3d(data))
}
pub fn prc(rect: Rect, data: Vec<u8>) -> Self {
Self::new(rect, ThreeDStream::prc(data))
}
pub fn with_view(mut self, view: ThreeDView) -> Self {
self.default_view = view;
self
}
pub fn add_view(mut self, view: ThreeDView) -> Self {
self.views.push(view);
self
}
pub fn with_camera_distance(mut self, distance: f32) -> Self {
self.default_view.camera.distance = distance;
self
}
pub fn with_render_mode(mut self, mode: ThreeDRenderMode) -> Self {
self.default_view.render_mode = mode;
self
}
pub fn with_lighting(mut self, lighting: ThreeDLighting) -> Self {
self.default_view.lighting = lighting;
self
}
pub fn with_background(mut self, background: ThreeDBackground) -> Self {
self.default_view.background = Some(background);
self
}
pub fn with_activation(mut self, activation: ThreeDActivation) -> Self {
self.activation = activation;
self
}
pub fn with_deactivation(mut self, deactivation: ThreeDDeactivation) -> Self {
self.deactivation = deactivation;
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
self.contents = Some(contents.into());
self
}
pub fn with_flags(mut self, flags: AnnotationFlags) -> Self {
self.flags = flags;
self
}
pub fn with_interactive(mut self, interactive: bool) -> Self {
self.interactive = interactive;
self
}
pub fn with_toolbar(mut self, toolbar: bool) -> Self {
self.toolbar = toolbar;
self
}
pub fn with_nav_panel(mut self, nav_panel: bool) -> Self {
self.nav_panel = nav_panel;
self
}
pub fn build(&self, _page_refs: &[ObjectRef]) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
dict.insert("Subtype".to_string(), Object::Name("3D".to_string()));
dict.insert(
"Rect".to_string(),
Object::Array(vec![
Object::Real(self.rect.x as f64),
Object::Real(self.rect.y as f64),
Object::Real((self.rect.x + self.rect.width) as f64),
Object::Real((self.rect.y + self.rect.height) as f64),
]),
);
if let Some(ref title) = self.title {
dict.insert("T".to_string(), Object::String(title.as_bytes().to_vec()));
}
if self.flags.bits() != 0 {
dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
}
if let Some(ref contents) = self.contents {
dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
}
let mut activation = HashMap::new();
activation.insert("A".to_string(), Object::Name(self.activation.pdf_name().to_string()));
activation.insert("D".to_string(), Object::Name(self.deactivation.pdf_name().to_string()));
if self.interactive {
activation.insert("AIS".to_string(), Object::Name("I".to_string()));
} else {
activation.insert("AIS".to_string(), Object::Name("L".to_string()));
}
activation.insert("DIS".to_string(), Object::Name("U".to_string()));
activation.insert("TB".to_string(), Object::Boolean(self.toolbar));
activation.insert("NP".to_string(), Object::Boolean(self.nav_panel));
dict.insert("3DA".to_string(), Object::Dictionary(activation));
dict.insert("3DV".to_string(), Object::Dictionary(self.default_view.build()));
dict
}
pub fn build_stream(&self) -> (HashMap<String, Object>, Vec<u8>) {
(self.stream.build(), self.stream.data.clone())
}
pub fn stream(&self) -> &ThreeDStream {
&self.stream
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_threed_format() {
assert_eq!(ThreeDFormat::U3D.subtype(), "U3D");
assert_eq!(ThreeDFormat::PRC.subtype(), "PRC");
}
#[test]
fn test_threed_lighting() {
assert_eq!(ThreeDLighting::White.pdf_name(), "White");
assert_eq!(ThreeDLighting::CAD.pdf_name(), "CAD");
assert_eq!(ThreeDLighting::Headlamp.pdf_name(), "Headlamp");
}
#[test]
fn test_threed_render_mode() {
assert_eq!(ThreeDRenderMode::Solid.pdf_name(), "Solid");
assert_eq!(ThreeDRenderMode::Wireframe.pdf_name(), "Wireframe");
assert_eq!(ThreeDRenderMode::Illustration.pdf_name(), "Illustration");
}
#[test]
fn test_threed_projection() {
assert_eq!(ThreeDProjection::Perspective.pdf_name(), "Perspective");
assert_eq!(ThreeDProjection::Orthographic.pdf_name(), "Orthographic");
}
#[test]
fn test_threed_activation() {
assert_eq!(ThreeDActivation::PageVisible.pdf_name(), "PV");
assert_eq!(ThreeDActivation::PageOpen.pdf_name(), "PO");
assert_eq!(ThreeDActivation::Explicit.pdf_name(), "XA");
}
#[test]
fn test_threed_deactivation() {
assert_eq!(ThreeDDeactivation::PageNotVisible.pdf_name(), "PI");
assert_eq!(ThreeDDeactivation::PageClose.pdf_name(), "PC");
assert_eq!(ThreeDDeactivation::Explicit.pdf_name(), "XD");
}
#[test]
fn test_threed_background() {
let bg = ThreeDBackground::new(0.5, 0.5, 0.5);
assert_eq!(bg.color, (0.5, 0.5, 0.5));
let dict = bg.build();
assert_eq!(dict.get("Type"), Some(&Object::Name("3DBG".to_string())));
}
#[test]
fn test_threed_camera_default() {
let camera = ThreeDCamera::default();
assert_eq!(camera.distance, 100.0);
assert_eq!(camera.azimuth, 45.0);
assert_eq!(camera.elevation, 30.0);
}
#[test]
fn test_threed_camera_builder() {
let camera = ThreeDCamera::new()
.with_distance(200.0)
.with_azimuth(90.0)
.with_elevation(45.0)
.with_center(10.0, 20.0, 30.0);
assert_eq!(camera.distance, 200.0);
assert_eq!(camera.azimuth, 90.0);
assert_eq!(camera.elevation, 45.0);
assert_eq!(camera.center, (10.0, 20.0, 30.0));
}
#[test]
fn test_threed_camera_matrix() {
let camera = ThreeDCamera::default();
let matrix = camera.calculate_matrix();
assert_eq!(matrix.len(), 12);
}
#[test]
fn test_threed_view_default() {
let view = ThreeDView::default_view();
assert_eq!(view.name, Some("Default".to_string()));
assert!(matches!(view.projection, ThreeDProjection::Perspective));
assert!(matches!(view.render_mode, ThreeDRenderMode::Solid));
}
#[test]
fn test_threed_view_presets() {
let front = ThreeDView::front();
assert_eq!(front.name, Some("Front".to_string()));
assert_eq!(front.camera.azimuth, 0.0);
let top = ThreeDView::top();
assert_eq!(top.name, Some("Top".to_string()));
assert_eq!(top.camera.elevation, 90.0);
let right = ThreeDView::right();
assert_eq!(right.name, Some("Right".to_string()));
assert_eq!(right.camera.azimuth, 90.0);
}
#[test]
fn test_threed_view_builder() {
let view = ThreeDView::new()
.with_name("Custom View")
.with_camera_distance(150.0)
.with_render_mode(ThreeDRenderMode::Wireframe)
.with_lighting(ThreeDLighting::CAD)
.with_background(ThreeDBackground::gray(0.8));
assert_eq!(view.name, Some("Custom View".to_string()));
assert_eq!(view.camera.distance, 150.0);
assert!(matches!(view.render_mode, ThreeDRenderMode::Wireframe));
assert!(matches!(view.lighting, ThreeDLighting::CAD));
assert!(view.background.is_some());
}
#[test]
fn test_threed_view_build() {
let view = ThreeDView::default_view();
let dict = view.build();
assert_eq!(dict.get("Type"), Some(&Object::Name("3DView".to_string())));
assert!(dict.contains_key("XN"));
assert!(dict.contains_key("C2W"));
assert!(dict.contains_key("P"));
assert!(dict.contains_key("RM"));
assert!(dict.contains_key("LS"));
}
#[test]
fn test_threed_stream_u3d() {
let data = vec![0u8; 1000];
let stream = ThreeDStream::u3d(data.clone());
assert_eq!(stream.data, data);
assert!(matches!(stream.format, ThreeDFormat::U3D));
}
#[test]
fn test_threed_stream_prc() {
let data = vec![0u8; 500];
let stream = ThreeDStream::prc(data.clone());
assert_eq!(stream.data, data);
assert!(matches!(stream.format, ThreeDFormat::PRC));
}
#[test]
fn test_threed_stream_build() {
let stream = ThreeDStream::u3d(vec![]);
let dict = stream.build();
assert_eq!(dict.get("Type"), Some(&Object::Name("3D".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("U3D".to_string())));
}
#[test]
fn test_threed_annotation_new() {
let rect = Rect::new(72.0, 400.0, 400.0, 300.0);
let annot = ThreeDAnnotation::u3d(rect, vec![0u8; 100]);
assert_eq!(annot.rect.x, 72.0);
assert_eq!(annot.rect.width, 400.0);
assert!(annot.interactive);
assert!(annot.toolbar);
}
#[test]
fn test_threed_annotation_builder() {
let rect = Rect::new(72.0, 400.0, 400.0, 300.0);
let annot = ThreeDAnnotation::u3d(rect, vec![])
.with_title("3D Model")
.with_camera_distance(200.0)
.with_render_mode(ThreeDRenderMode::SolidWireframe)
.with_lighting(ThreeDLighting::CAD)
.with_background(ThreeDBackground::gray(0.9))
.with_activation(ThreeDActivation::Explicit)
.with_toolbar(true)
.with_nav_panel(true);
assert_eq!(annot.title, Some("3D Model".to_string()));
assert_eq!(annot.default_view.camera.distance, 200.0);
assert!(matches!(annot.default_view.render_mode, ThreeDRenderMode::SolidWireframe));
assert!(matches!(annot.activation, ThreeDActivation::Explicit));
assert!(annot.toolbar);
assert!(annot.nav_panel);
}
#[test]
fn test_threed_annotation_add_views() {
let rect = Rect::new(72.0, 400.0, 400.0, 300.0);
let annot = ThreeDAnnotation::u3d(rect, vec![])
.add_view(ThreeDView::front())
.add_view(ThreeDView::top())
.add_view(ThreeDView::right());
assert_eq!(annot.views.len(), 3);
}
#[test]
fn test_threed_annotation_build() {
let rect = Rect::new(72.0, 400.0, 400.0, 300.0);
let annot = ThreeDAnnotation::u3d(rect, vec![1, 2, 3])
.with_title("Test Model")
.with_contents("A 3D model");
let dict = annot.build(&[]);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("3D".to_string())));
assert!(dict.contains_key("Rect"));
assert!(dict.contains_key("T")); assert!(dict.contains_key("3DA")); assert!(dict.contains_key("3DV")); }
}