use crate::entities::{Entity, EntityCommon};
use crate::types::{BoundingBox3D, Color, Handle, LineWeight, Transparency, Vector3};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ViewportStatusFlags {
pub is_on: bool,
pub perspective: bool,
pub front_clipping: bool,
pub back_clipping: bool,
pub ucs_follow: bool,
pub front_clip_not_at_eye: bool,
pub ucs_icon_visible: bool,
pub ucs_icon_at_origin: bool,
pub fast_zoom: bool,
pub snap_on: bool,
pub grid_on: bool,
pub isometric_snap: bool,
pub hide_plot: bool,
pub iso_pair_top: bool,
pub iso_pair_right: bool,
pub locked: bool,
}
impl ViewportStatusFlags {
pub fn default_on() -> Self {
Self {
is_on: true,
..Default::default()
}
}
pub fn from_bits(bits: i32) -> Self {
Self {
is_on: (bits & (1 << 0)) != 0,
perspective: (bits & (1 << 1)) != 0,
front_clipping: (bits & (1 << 2)) != 0,
back_clipping: (bits & (1 << 3)) != 0,
ucs_follow: (bits & (1 << 4)) != 0,
front_clip_not_at_eye: (bits & (1 << 5)) != 0,
ucs_icon_visible: (bits & (1 << 6)) != 0,
ucs_icon_at_origin: (bits & (1 << 7)) != 0,
fast_zoom: (bits & (1 << 8)) != 0,
snap_on: (bits & (1 << 9)) != 0,
grid_on: (bits & (1 << 10)) != 0,
isometric_snap: (bits & (1 << 11)) != 0,
hide_plot: (bits & (1 << 12)) != 0,
iso_pair_top: (bits & (1 << 13)) != 0,
iso_pair_right: (bits & (1 << 14)) != 0,
locked: (bits & (1 << 15)) != 0,
}
}
pub fn to_bits(&self) -> i32 {
let mut bits = 0;
if self.is_on { bits |= 1 << 0; }
if self.perspective { bits |= 1 << 1; }
if self.front_clipping { bits |= 1 << 2; }
if self.back_clipping { bits |= 1 << 3; }
if self.ucs_follow { bits |= 1 << 4; }
if self.front_clip_not_at_eye { bits |= 1 << 5; }
if self.ucs_icon_visible { bits |= 1 << 6; }
if self.ucs_icon_at_origin { bits |= 1 << 7; }
if self.fast_zoom { bits |= 1 << 8; }
if self.snap_on { bits |= 1 << 9; }
if self.grid_on { bits |= 1 << 10; }
if self.isometric_snap { bits |= 1 << 11; }
if self.hide_plot { bits |= 1 << 12; }
if self.iso_pair_top { bits |= 1 << 13; }
if self.iso_pair_right { bits |= 1 << 14; }
if self.locked { bits |= 1 << 15; }
bits
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ViewportRenderMode {
#[default]
Wireframe2D = 0,
Wireframe3D = 1,
HiddenLine = 2,
FlatShaded = 3,
GouraudShaded = 4,
FlatShadedWithEdges = 5,
GouraudShadedWithEdges = 6,
}
impl ViewportRenderMode {
pub fn from_value(value: i16) -> Self {
match value {
1 => ViewportRenderMode::Wireframe3D,
2 => ViewportRenderMode::HiddenLine,
3 => ViewportRenderMode::FlatShaded,
4 => ViewportRenderMode::GouraudShaded,
5 => ViewportRenderMode::FlatShadedWithEdges,
6 => ViewportRenderMode::GouraudShadedWithEdges,
_ => ViewportRenderMode::Wireframe2D,
}
}
pub fn to_value(&self) -> i16 {
*self as i16
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GridFlags {
pub beyond_limits: bool,
pub adaptive: bool,
pub subdivision: bool,
pub follow_dynamic: bool,
}
impl GridFlags {
pub fn from_bits(bits: i16) -> Self {
Self {
beyond_limits: (bits & 1) != 0,
adaptive: (bits & 2) != 0,
subdivision: (bits & 4) != 0,
follow_dynamic: (bits & 8) != 0,
}
}
pub fn to_bits(&self) -> i16 {
let mut bits = 0;
if self.beyond_limits { bits |= 1; }
if self.adaptive { bits |= 2; }
if self.subdivision { bits |= 4; }
if self.follow_dynamic { bits |= 8; }
bits
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Viewport {
pub common: EntityCommon,
pub center: Vector3,
pub width: f64,
pub height: f64,
pub status: ViewportStatusFlags,
pub id: i16,
pub view_center: Vector3,
pub snap_base: Vector3,
pub snap_spacing: Vector3,
pub grid_spacing: Vector3,
pub view_direction: Vector3,
pub view_target: Vector3,
pub lens_length: f64,
pub front_clip_z: f64,
pub back_clip_z: f64,
pub view_height: f64,
pub snap_angle: f64,
pub twist_angle: f64,
pub circle_sides: i16,
pub frozen_layers: Vec<Handle>,
pub render_mode: ViewportRenderMode,
pub ucs_per_viewport: bool,
pub ucs_icon_visible: bool,
pub ucs_origin: Vector3,
pub ucs_x_axis: Vector3,
pub ucs_y_axis: Vector3,
pub ucs_handle: Handle,
pub base_ucs_handle: Handle,
pub ucs_ortho_type: i16,
pub elevation: f64,
pub shade_plot_mode: i16,
pub grid_flags: GridFlags,
pub grid_major: i16,
pub background_handle: Handle,
pub shade_plot_handle: Handle,
pub visual_style_handle: Handle,
pub default_lighting: bool,
pub default_lighting_type: i16,
pub brightness: f64,
pub contrast: f64,
pub ambient_color: i32,
pub custom_scale: f64,
}
impl Viewport {
pub fn new() -> Self {
Self {
common: EntityCommon::default(),
center: Vector3::ZERO,
width: 297.0, height: 210.0, status: ViewportStatusFlags::default_on(),
id: 0,
view_center: Vector3::ZERO,
snap_base: Vector3::ZERO,
snap_spacing: Vector3::new(10.0, 10.0, 0.0),
grid_spacing: Vector3::new(10.0, 10.0, 0.0),
view_direction: Vector3::UNIT_Z,
view_target: Vector3::ZERO,
lens_length: 50.0,
front_clip_z: 0.0,
back_clip_z: 0.0,
view_height: 210.0,
snap_angle: 0.0,
twist_angle: 0.0,
circle_sides: 1000,
frozen_layers: Vec::new(),
render_mode: ViewportRenderMode::Wireframe2D,
ucs_per_viewport: false,
ucs_icon_visible: true,
ucs_origin: Vector3::ZERO,
ucs_x_axis: Vector3::UNIT_X,
ucs_y_axis: Vector3::UNIT_Y,
ucs_handle: Handle::NULL,
base_ucs_handle: Handle::NULL,
ucs_ortho_type: 0,
elevation: 0.0,
shade_plot_mode: 0,
grid_flags: GridFlags::default(),
grid_major: 5,
background_handle: Handle::NULL,
shade_plot_handle: Handle::NULL,
visual_style_handle: Handle::NULL,
default_lighting: true,
default_lighting_type: 1,
brightness: 0.0,
contrast: 0.0,
ambient_color: 0,
custom_scale: 1.0,
}
}
pub fn with_size(center: Vector3, width: f64, height: f64) -> Self {
let mut vp = Self::new();
vp.center = center;
vp.width = width;
vp.height = height;
vp
}
pub fn model_space() -> Self {
let mut vp = Self::new();
vp.id = 1;
vp
}
pub fn set_view_target(&mut self, target: Vector3) {
self.view_target = target;
}
pub fn set_view_direction(&mut self, direction: Vector3) {
let normalized = direction.normalize();
if normalized.length_squared() > 0.0 {
self.view_direction = normalized;
} else {
self.view_direction = Vector3::UNIT_Z;
}
}
pub fn set_view_height(&mut self, height: f64) {
self.view_height = height;
}
pub fn scale(&self) -> f64 {
if self.view_height.abs() > 1e-10 {
self.height / self.view_height
} else {
1.0
}
}
pub fn set_scale(&mut self, scale: f64) {
if scale.abs() > 1e-10 {
self.view_height = self.height / scale;
self.custom_scale = scale;
}
}
pub fn lock(&mut self) {
self.status.locked = true;
}
pub fn unlock(&mut self) {
self.status.locked = false;
}
pub fn is_locked(&self) -> bool {
self.status.locked
}
pub fn turn_on(&mut self) {
self.status.is_on = true;
}
pub fn turn_off(&mut self) {
self.status.is_on = false;
}
pub fn is_on(&self) -> bool {
self.status.is_on
}
pub fn freeze_layer(&mut self, layer_handle: Handle) {
if !self.frozen_layers.contains(&layer_handle) {
self.frozen_layers.push(layer_handle);
}
}
pub fn thaw_layer(&mut self, layer_handle: Handle) {
self.frozen_layers.retain(|h| *h != layer_handle);
}
pub fn set_render_mode(&mut self, mode: ViewportRenderMode) {
self.render_mode = mode;
}
pub fn paper_bounds(&self) -> BoundingBox3D {
let half_w = self.width / 2.0;
let half_h = self.height / 2.0;
BoundingBox3D::new(
Vector3::new(self.center.x - half_w, self.center.y - half_h, 0.0),
Vector3::new(self.center.x + half_w, self.center.y + half_h, 0.0),
)
}
pub fn set_standard_view(&mut self, view: StandardView) {
match view {
StandardView::Top => {
self.view_direction = Vector3::UNIT_Z;
self.twist_angle = 0.0;
}
StandardView::Bottom => {
self.view_direction = -Vector3::UNIT_Z;
self.twist_angle = 0.0;
}
StandardView::Front => {
self.view_direction = -Vector3::UNIT_Y;
self.twist_angle = 0.0;
}
StandardView::Back => {
self.view_direction = Vector3::UNIT_Y;
self.twist_angle = 0.0;
}
StandardView::Left => {
self.view_direction = -Vector3::UNIT_X;
self.twist_angle = 0.0;
}
StandardView::Right => {
self.view_direction = Vector3::UNIT_X;
self.twist_angle = 0.0;
}
StandardView::SWIsometric => {
self.view_direction = Vector3::new(-1.0, -1.0, 1.0).normalize();
self.twist_angle = 0.0;
}
StandardView::SEIsometric => {
self.view_direction = Vector3::new(1.0, -1.0, 1.0).normalize();
self.twist_angle = 0.0;
}
StandardView::NEIsometric => {
self.view_direction = Vector3::new(1.0, 1.0, 1.0).normalize();
self.twist_angle = 0.0;
}
StandardView::NWIsometric => {
self.view_direction = Vector3::new(-1.0, 1.0, 1.0).normalize();
self.twist_angle = 0.0;
}
}
}
pub fn with_center(mut self, center: Vector3) -> Self {
self.center = center;
self
}
pub fn with_view_target(mut self, target: Vector3) -> Self {
self.view_target = target;
self
}
pub fn with_view_direction(mut self, direction: Vector3) -> Self {
let normalized = direction.normalize();
if normalized.length_squared() > 0.0 {
self.view_direction = normalized;
} else {
self.view_direction = Vector3::UNIT_Z;
}
self
}
pub fn with_scale(mut self, scale: f64) -> Self {
self.set_scale(scale);
self
}
pub fn with_locked(mut self) -> Self {
self.status.locked = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StandardView {
Top,
Bottom,
Front,
Back,
Left,
Right,
SWIsometric,
SEIsometric,
NEIsometric,
NWIsometric,
}
impl Default for Viewport {
fn default() -> Self {
Self::new()
}
}
impl Entity for Viewport {
fn handle(&self) -> Handle {
self.common.handle
}
fn set_handle(&mut self, handle: Handle) {
self.common.handle = handle;
}
fn layer(&self) -> &str {
&self.common.layer
}
fn set_layer(&mut self, layer: String) {
self.common.layer = layer;
}
fn color(&self) -> Color {
self.common.color
}
fn set_color(&mut self, color: Color) {
self.common.color = color;
}
fn line_weight(&self) -> LineWeight {
self.common.line_weight
}
fn set_line_weight(&mut self, weight: LineWeight) {
self.common.line_weight = weight;
}
fn transparency(&self) -> Transparency {
self.common.transparency
}
fn set_transparency(&mut self, transparency: Transparency) {
self.common.transparency = transparency;
}
fn is_invisible(&self) -> bool {
self.common.invisible
}
fn set_invisible(&mut self, invisible: bool) {
self.common.invisible = invisible;
}
fn bounding_box(&self) -> BoundingBox3D {
self.paper_bounds()
}
fn translate(&mut self, offset: Vector3) {
super::translate::translate_viewport(self, offset);
}
fn entity_type(&self) -> &'static str {
"VIEWPORT"
}
fn apply_transform(&mut self, transform: &crate::types::Transform) {
super::transform::transform_viewport(self, transform);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viewport_creation() {
let vp = Viewport::new();
assert!(vp.is_on());
assert!(!vp.is_locked());
assert_eq!(vp.view_direction, Vector3::UNIT_Z);
}
#[test]
fn test_viewport_with_size() {
let vp = Viewport::with_size(
Vector3::new(100.0, 100.0, 0.0),
200.0,
150.0,
);
assert_eq!(vp.center, Vector3::new(100.0, 100.0, 0.0));
assert_eq!(vp.width, 200.0);
assert_eq!(vp.height, 150.0);
}
#[test]
fn test_viewport_scale() {
let mut vp = Viewport::new();
vp.height = 100.0;
vp.view_height = 200.0;
assert!((vp.scale() - 0.5).abs() < 1e-10);
vp.set_scale(0.25);
assert!((vp.view_height - 400.0).abs() < 1e-10);
}
#[test]
fn test_viewport_lock() {
let mut vp = Viewport::new();
assert!(!vp.is_locked());
vp.lock();
assert!(vp.is_locked());
vp.unlock();
assert!(!vp.is_locked());
}
#[test]
fn test_viewport_frozen_layers() {
let mut vp = Viewport::new();
let layer1 = Handle::new(100);
let layer2 = Handle::new(200);
vp.freeze_layer(layer1);
vp.freeze_layer(layer2);
assert_eq!(vp.frozen_layers.len(), 2);
vp.thaw_layer(layer1);
assert_eq!(vp.frozen_layers.len(), 1);
assert!(vp.frozen_layers.contains(&layer2));
}
#[test]
fn test_viewport_standard_views() {
let mut vp = Viewport::new();
vp.set_standard_view(StandardView::Front);
assert_eq!(vp.view_direction, -Vector3::UNIT_Y);
vp.set_standard_view(StandardView::Top);
assert_eq!(vp.view_direction, Vector3::UNIT_Z);
vp.set_standard_view(StandardView::NEIsometric);
let expected = Vector3::new(1.0, 1.0, 1.0).normalize();
assert!((vp.view_direction.x - expected.x).abs() < 1e-10);
assert!((vp.view_direction.y - expected.y).abs() < 1e-10);
assert!((vp.view_direction.z - expected.z).abs() < 1e-10);
}
#[test]
fn test_viewport_paper_bounds() {
let vp = Viewport::with_size(
Vector3::new(100.0, 100.0, 0.0),
200.0,
100.0,
);
let bounds = vp.paper_bounds();
assert_eq!(bounds.min.x, 0.0);
assert_eq!(bounds.min.y, 50.0);
assert_eq!(bounds.max.x, 200.0);
assert_eq!(bounds.max.y, 150.0);
}
#[test]
fn test_viewport_status_flags() {
let flags = ViewportStatusFlags::from_bits(0b1000000000000001);
assert!(flags.is_on);
assert!(flags.locked);
assert!(!flags.perspective);
assert_eq!(flags.to_bits(), 0b1000000000000001);
}
#[test]
fn test_viewport_render_mode() {
let mut vp = Viewport::new();
vp.set_render_mode(ViewportRenderMode::HiddenLine);
assert_eq!(vp.render_mode, ViewportRenderMode::HiddenLine);
}
#[test]
fn test_viewport_translate() {
let mut vp = Viewport::with_size(
Vector3::new(0.0, 0.0, 0.0),
100.0,
100.0,
);
vp.translate(Vector3::new(50.0, 50.0, 0.0));
assert_eq!(vp.center, Vector3::new(50.0, 50.0, 0.0));
}
#[test]
fn test_viewport_builder() {
let vp = Viewport::new()
.with_center(Vector3::new(100.0, 100.0, 0.0))
.with_view_target(Vector3::new(50.0, 50.0, 0.0))
.with_scale(0.5)
.with_locked();
assert_eq!(vp.center, Vector3::new(100.0, 100.0, 0.0));
assert_eq!(vp.view_target, Vector3::new(50.0, 50.0, 0.0));
assert!(vp.is_locked());
}
}