use crate::entities::{Entity, EntityCommon};
use crate::types::{BoundingBox3D, Color, Handle, LineWeight, Transparency, Vector2, Vector3};
use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum ClipMode {
#[default]
Outside = 0,
Inside = 1,
}
impl From<u8> for ClipMode {
fn from(value: u8) -> Self {
match value {
0 => Self::Outside,
1 => Self::Inside,
_ => Self::Outside,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(i16)]
pub enum ClipType {
#[default]
Rectangular = 1,
Polygonal = 2,
}
impl From<i16> for ClipType {
fn from(value: i16) -> Self {
match value {
1 => Self::Rectangular,
2 => Self::Polygonal,
_ => Self::Rectangular,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum ResolutionUnit {
#[default]
None = 0,
Centimeters = 2,
Inches = 5,
}
impl From<u8> for ResolutionUnit {
fn from(value: u8) -> Self {
match value {
0 => Self::None,
2 => Self::Centimeters,
5 => Self::Inches,
_ => Self::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(i32)]
pub enum ImageDisplayQuality {
Draft = 0,
#[default]
High = 1,
}
impl From<i32> for ImageDisplayQuality {
fn from(value: i32) -> Self {
match value {
0 => Self::Draft,
1 => Self::High,
_ => Self::High,
}
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageDisplayFlags: i16 {
const NONE = 0;
const SHOW_IMAGE = 1;
const SHOW_NOT_ALIGNED = 2;
const USE_CLIPPING_BOUNDARY = 4;
const TRANSPARENCY_ON = 8;
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClipBoundary {
pub clip_type: ClipType,
pub clip_mode: ClipMode,
pub vertices: Vec<Vector2>,
}
impl ClipBoundary {
pub fn rectangular(corner1: Vector2, corner2: Vector2) -> Self {
Self {
clip_type: ClipType::Rectangular,
clip_mode: ClipMode::Outside,
vertices: vec![corner1, corner2],
}
}
pub fn polygonal(vertices: Vec<Vector2>) -> Self {
Self {
clip_type: ClipType::Polygonal,
clip_mode: ClipMode::Outside,
vertices,
}
}
pub fn full_image(width: f64, height: f64) -> Self {
Self::rectangular(
Vector2::new(-0.5, -0.5),
Vector2::new(width - 0.5, height - 0.5),
)
}
pub fn vertex_count(&self) -> usize {
self.vertices.len()
}
pub fn is_rectangular(&self) -> bool {
self.clip_type == ClipType::Rectangular
}
pub fn is_polygonal(&self) -> bool {
self.clip_type == ClipType::Polygonal
}
pub fn bounding_rect(&self) -> Option<(Vector2, Vector2)> {
if self.vertices.is_empty() {
return None;
}
let mut min = self.vertices[0];
let mut max = self.vertices[0];
for v in &self.vertices[1..] {
min.x = min.x.min(v.x);
min.y = min.y.min(v.y);
max.x = max.x.max(v.x);
max.y = max.y.max(v.y);
}
Some((min, max))
}
}
impl Default for ClipBoundary {
fn default() -> Self {
Self {
clip_type: ClipType::Rectangular,
clip_mode: ClipMode::Outside,
vertices: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RasterImage {
pub common: EntityCommon,
pub class_version: i32,
pub insertion_point: Vector3,
pub u_vector: Vector3,
pub v_vector: Vector3,
pub size: Vector2,
pub flags: ImageDisplayFlags,
pub clipping_enabled: bool,
pub brightness: u8,
pub contrast: u8,
pub fade: u8,
pub clip_boundary: ClipBoundary,
pub definition_handle: Option<Handle>,
pub definition_reactor_handle: Option<Handle>,
pub file_path: String,
}
impl RasterImage {
pub fn new(file_path: &str, insertion_point: Vector3, width_pixels: f64, height_pixels: f64) -> Self {
let size = Vector2::new(width_pixels, height_pixels);
Self {
common: EntityCommon::default(),
class_version: 0,
insertion_point,
u_vector: Vector3::new(1.0, 0.0, 0.0),
v_vector: Vector3::new(0.0, 1.0, 0.0),
size,
flags: ImageDisplayFlags::SHOW_IMAGE | ImageDisplayFlags::USE_CLIPPING_BOUNDARY,
clipping_enabled: false,
brightness: 50,
contrast: 50,
fade: 0,
clip_boundary: ClipBoundary::full_image(width_pixels, height_pixels),
definition_handle: None,
definition_reactor_handle: None,
file_path: file_path.to_string(),
}
}
pub fn with_size(
file_path: &str,
insertion_point: Vector3,
width_pixels: f64,
height_pixels: f64,
world_width: f64,
world_height: f64,
) -> Self {
let mut image = Self::new(file_path, insertion_point, width_pixels, height_pixels);
image.set_size(world_width, world_height);
image
}
pub fn width(&self) -> f64 {
self.u_vector.length() * self.size.x
}
pub fn height(&self) -> f64 {
self.v_vector.length() * self.size.y
}
pub fn aspect_ratio(&self) -> f64 {
if self.size.y == 0.0 {
1.0
} else {
self.size.x / self.size.y
}
}
pub fn set_width(&mut self, width: f64) {
let pixel_size = width / self.size.x;
self.u_vector = self.u_vector.normalize() * pixel_size;
}
pub fn set_height(&mut self, height: f64) {
let pixel_size = height / self.size.y;
self.v_vector = self.v_vector.normalize() * pixel_size;
}
pub fn set_size(&mut self, width: f64, height: f64) {
self.set_width(width);
self.set_height(height);
}
pub fn set_width_keep_aspect(&mut self, width: f64) {
let ratio = self.aspect_ratio();
self.set_width(width);
self.set_height(width / ratio);
}
pub fn set_height_keep_aspect(&mut self, height: f64) {
let ratio = self.aspect_ratio();
self.set_height(height);
self.set_width(height * ratio);
}
pub fn pixel_size(&self) -> (f64, f64) {
(self.u_vector.length(), self.v_vector.length())
}
pub fn set_pixel_size(&mut self, size: f64) {
self.u_vector = self.u_vector.normalize() * size;
self.v_vector = self.v_vector.normalize() * size;
}
pub fn is_visible(&self) -> bool {
self.flags.contains(ImageDisplayFlags::SHOW_IMAGE)
}
pub fn set_visible(&mut self, visible: bool) {
if visible {
self.flags |= ImageDisplayFlags::SHOW_IMAGE;
} else {
self.flags &= !ImageDisplayFlags::SHOW_IMAGE;
}
}
pub fn is_transparent(&self) -> bool {
self.flags.contains(ImageDisplayFlags::TRANSPARENCY_ON)
}
pub fn set_transparent(&mut self, transparent: bool) {
if transparent {
self.flags |= ImageDisplayFlags::TRANSPARENCY_ON;
} else {
self.flags &= !ImageDisplayFlags::TRANSPARENCY_ON;
}
}
pub fn corners(&self) -> [Vector3; 4] {
let origin = self.insertion_point;
let u = self.u_vector * self.size.x;
let v = self.v_vector * self.size.y;
[
origin, origin + u, origin + u + v, origin + v, ]
}
pub fn center(&self) -> Vector3 {
let u = self.u_vector * self.size.x;
let v = self.v_vector * self.size.y;
self.insertion_point + (u + v) * 0.5
}
pub fn bounding_box(&self) -> (Vector3, Vector3) {
let corners = self.corners();
let mut min = corners[0];
let mut max = corners[0];
for c in &corners[1..] {
min.x = min.x.min(c.x);
min.y = min.y.min(c.y);
min.z = min.z.min(c.z);
max.x = max.x.max(c.x);
max.y = max.y.max(c.y);
max.z = max.z.max(c.z);
}
(min, max)
}
pub fn translate(&mut self, offset: Vector3) {
self.insertion_point = self.insertion_point + offset;
}
pub fn rotate(&mut self, angle: f64) {
let cos_a = angle.cos();
let sin_a = angle.sin();
let u = self.u_vector;
self.u_vector = Vector3::new(
u.x * cos_a - u.y * sin_a,
u.x * sin_a + u.y * cos_a,
u.z,
);
let v = self.v_vector;
self.v_vector = Vector3::new(
v.x * cos_a - v.y * sin_a,
v.x * sin_a + v.y * cos_a,
v.z,
);
}
pub fn set_clip_rect(&mut self, corner1: Vector2, corner2: Vector2) {
self.clip_boundary = ClipBoundary::rectangular(corner1, corner2);
self.clipping_enabled = true;
}
pub fn set_clip_polygon(&mut self, vertices: Vec<Vector2>) {
self.clip_boundary = ClipBoundary::polygonal(vertices);
self.clipping_enabled = true;
}
pub fn clear_clip(&mut self) {
self.clip_boundary = ClipBoundary::full_image(self.size.x, self.size.y);
self.clipping_enabled = false;
}
pub fn file_name(&self) -> &str {
self.file_path
.rsplit(['/', '\\'])
.next()
.unwrap_or(&self.file_path)
}
pub fn file_extension(&self) -> Option<&str> {
self.file_path.rsplit('.').next()
}
}
impl Default for RasterImage {
fn default() -> Self {
Self::new("", Vector3::ZERO, 1.0, 1.0)
}
}
impl Entity for RasterImage {
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, line_weight: LineWeight) {
self.common.line_weight = line_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 {
let corners = self.corners();
let mut min = corners[0];
let mut max = corners[0];
for c in &corners[1..] {
min.x = min.x.min(c.x);
min.y = min.y.min(c.y);
min.z = min.z.min(c.z);
max.x = max.x.max(c.x);
max.y = max.y.max(c.y);
max.z = max.z.max(c.z);
}
BoundingBox3D::new(min, max)
}
fn translate(&mut self, offset: Vector3) {
super::translate::translate_raster_image(self, offset);
}
fn entity_type(&self) -> &'static str {
"IMAGE"
}
fn apply_transform(&mut self, transform: &crate::types::Transform) {
super::transform::transform_raster_image(self, transform);
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ImageDefinition {
pub handle: Handle,
pub class_version: i32,
pub file_path: String,
pub size: Vector2,
pub pixel_size: Vector2,
pub is_loaded: bool,
pub resolution_unit: ResolutionUnit,
}
impl ImageDefinition {
pub fn new(file_path: &str, width_pixels: f64, height_pixels: f64) -> Self {
Self {
handle: Handle::NULL,
class_version: 0,
file_path: file_path.to_string(),
size: Vector2::new(width_pixels, height_pixels),
pixel_size: Vector2::new(1.0, 1.0),
is_loaded: true,
resolution_unit: ResolutionUnit::None,
}
}
pub fn aspect_ratio(&self) -> f64 {
if self.size.y == 0.0 {
1.0
} else {
self.size.x / self.size.y
}
}
pub fn width(&self) -> f64 {
self.size.x
}
pub fn height(&self) -> f64 {
self.size.y
}
pub fn file_name(&self) -> &str {
self.file_path
.rsplit(['/', '\\'])
.next()
.unwrap_or(&self.file_path)
}
pub fn set_dpi(&mut self, dpi: f64) {
self.resolution_unit = ResolutionUnit::Inches;
self.pixel_size = Vector2::new(1.0 / dpi, 1.0 / dpi);
}
pub fn dpi(&self) -> Option<f64> {
match self.resolution_unit {
ResolutionUnit::Inches if self.pixel_size.x > 0.0 => Some(1.0 / self.pixel_size.x),
_ => None,
}
}
}
impl Default for ImageDefinition {
fn default() -> Self {
Self::new("", 1.0, 1.0)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RasterImageBuilder {
image: RasterImage,
}
impl RasterImageBuilder {
pub fn new(file_path: &str, width_pixels: f64, height_pixels: f64) -> Self {
Self {
image: RasterImage::new(file_path, Vector3::ZERO, width_pixels, height_pixels),
}
}
pub fn at(mut self, point: Vector3) -> Self {
self.image.insertion_point = point;
self
}
pub fn size(mut self, width: f64, height: f64) -> Self {
self.image.set_size(width, height);
self
}
pub fn width(mut self, width: f64) -> Self {
self.image.set_width_keep_aspect(width);
self
}
pub fn height(mut self, height: f64) -> Self {
self.image.set_height_keep_aspect(height);
self
}
pub fn rotation(mut self, angle: f64) -> Self {
self.image.rotate(angle);
self
}
pub fn brightness(mut self, brightness: u8) -> Self {
self.image.brightness = brightness.min(100);
self
}
pub fn contrast(mut self, contrast: u8) -> Self {
self.image.contrast = contrast.min(100);
self
}
pub fn fade(mut self, fade: u8) -> Self {
self.image.fade = fade.min(100);
self
}
pub fn transparent(mut self) -> Self {
self.image.set_transparent(true);
self
}
pub fn clip_rect(mut self, corner1: Vector2, corner2: Vector2) -> Self {
self.image.set_clip_rect(corner1, corner2);
self
}
pub fn build(self) -> RasterImage {
self.image
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_raster_image_creation() {
let image = RasterImage::new("test.jpg", Vector3::ZERO, 1920.0, 1080.0);
assert_eq!(image.file_path, "test.jpg");
assert_eq!(image.size, Vector2::new(1920.0, 1080.0));
assert!(image.is_visible());
}
#[test]
fn test_raster_image_size() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 50.0);
assert!((image.width() - 100.0).abs() < 1e-10);
assert!((image.height() - 50.0).abs() < 1e-10);
image.set_width(10.0);
assert!((image.width() - 10.0).abs() < 1e-10);
}
#[test]
fn test_raster_image_aspect_ratio() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 1920.0, 1080.0);
let aspect = image.aspect_ratio();
image.set_width_keep_aspect(19.2);
assert!((image.width() - 19.2).abs() < 1e-10);
assert!((image.height() - 10.8).abs() < 0.01);
assert!((image.aspect_ratio() - aspect).abs() < 1e-10);
}
#[test]
fn test_raster_image_corners() {
let mut image = RasterImage::new("test.jpg", Vector3::new(10.0, 20.0, 0.0), 100.0, 50.0);
image.set_size(10.0, 5.0);
let corners = image.corners();
assert_eq!(corners[0], Vector3::new(10.0, 20.0, 0.0));
assert_eq!(corners[1], Vector3::new(20.0, 20.0, 0.0));
assert_eq!(corners[2], Vector3::new(20.0, 25.0, 0.0));
assert_eq!(corners[3], Vector3::new(10.0, 25.0, 0.0));
}
#[test]
fn test_raster_image_center() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 100.0);
image.set_size(10.0, 10.0);
let center = image.center();
assert_eq!(center, Vector3::new(5.0, 5.0, 0.0));
}
#[test]
fn test_raster_image_translate() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 100.0);
image.translate(Vector3::new(5.0, 10.0, 0.0));
assert_eq!(image.insertion_point, Vector3::new(5.0, 10.0, 0.0));
}
#[test]
fn test_raster_image_rotate() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 100.0);
image.rotate(PI / 2.0);
assert!((image.u_vector.x).abs() < 1e-10);
assert!((image.u_vector.y - 1.0).abs() < 1e-10);
}
#[test]
fn test_raster_image_visibility() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 100.0);
assert!(image.is_visible());
image.set_visible(false);
assert!(!image.is_visible());
image.set_visible(true);
assert!(image.is_visible());
}
#[test]
fn test_raster_image_transparency() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 100.0);
assert!(!image.is_transparent());
image.set_transparent(true);
assert!(image.is_transparent());
}
#[test]
fn test_raster_image_clipping() {
let mut image = RasterImage::new("test.jpg", Vector3::ZERO, 100.0, 100.0);
image.set_clip_rect(Vector2::new(10.0, 10.0), Vector2::new(90.0, 90.0));
assert!(image.clipping_enabled);
assert!(image.clip_boundary.is_rectangular());
image.set_clip_polygon(vec![
Vector2::new(50.0, 0.0),
Vector2::new(100.0, 50.0),
Vector2::new(50.0, 100.0),
Vector2::new(0.0, 50.0),
]);
assert!(image.clip_boundary.is_polygonal());
assert_eq!(image.clip_boundary.vertex_count(), 4);
image.clear_clip();
assert!(!image.clipping_enabled);
}
#[test]
fn test_clip_boundary_rectangular() {
let boundary = ClipBoundary::rectangular(
Vector2::new(0.0, 0.0),
Vector2::new(100.0, 50.0),
);
assert!(boundary.is_rectangular());
assert!(!boundary.is_polygonal());
assert_eq!(boundary.vertex_count(), 2);
let rect = boundary.bounding_rect().unwrap();
assert_eq!(rect.0, Vector2::new(0.0, 0.0));
assert_eq!(rect.1, Vector2::new(100.0, 50.0));
}
#[test]
fn test_clip_boundary_full_image() {
let boundary = ClipBoundary::full_image(100.0, 50.0);
assert_eq!(boundary.vertices[0], Vector2::new(-0.5, -0.5));
assert_eq!(boundary.vertices[1], Vector2::new(99.5, 49.5));
}
#[test]
fn test_raster_image_file_name() {
let image = RasterImage::new("C:/images/photo.jpg", Vector3::ZERO, 100.0, 100.0);
assert_eq!(image.file_name(), "photo.jpg");
assert_eq!(image.file_extension(), Some("jpg"));
}
#[test]
fn test_image_definition() {
let mut def = ImageDefinition::new("photo.png", 1920.0, 1080.0);
assert_eq!(def.file_name(), "photo.png");
assert_eq!(def.width(), 1920.0);
assert_eq!(def.height(), 1080.0);
assert!((def.aspect_ratio() - 1.7777777).abs() < 0.0001);
def.set_dpi(300.0);
assert_eq!(def.resolution_unit, ResolutionUnit::Inches);
assert!((def.dpi().unwrap() - 300.0).abs() < 1e-10);
}
#[test]
fn test_raster_image_builder() {
let image = RasterImageBuilder::new("photo.jpg", 1920.0, 1080.0)
.at(Vector3::new(10.0, 20.0, 0.0))
.width(19.2)
.brightness(70)
.contrast(60)
.transparent()
.build();
assert_eq!(image.insertion_point, Vector3::new(10.0, 20.0, 0.0));
assert!((image.width() - 19.2).abs() < 0.01);
assert_eq!(image.brightness, 70);
assert_eq!(image.contrast, 60);
assert!(image.is_transparent());
}
#[test]
fn test_display_flags() {
let flags = ImageDisplayFlags::SHOW_IMAGE | ImageDisplayFlags::TRANSPARENCY_ON;
assert!(flags.contains(ImageDisplayFlags::SHOW_IMAGE));
assert!(flags.contains(ImageDisplayFlags::TRANSPARENCY_ON));
assert!(!flags.contains(ImageDisplayFlags::USE_CLIPPING_BOUNDARY));
}
#[test]
fn test_clip_mode() {
assert_eq!(ClipMode::from(0), ClipMode::Outside);
assert_eq!(ClipMode::from(1), ClipMode::Inside);
}
#[test]
fn test_clip_type() {
assert_eq!(ClipType::from(1), ClipType::Rectangular);
assert_eq!(ClipType::from(2), ClipType::Polygonal);
}
#[test]
fn test_resolution_unit() {
assert_eq!(ResolutionUnit::from(0), ResolutionUnit::None);
assert_eq!(ResolutionUnit::from(2), ResolutionUnit::Centimeters);
assert_eq!(ResolutionUnit::from(5), ResolutionUnit::Inches);
}
}