use acadrust::entities::{Viewport, ViewportRenderMode}; use glam::Vec3; use crate::command::EntityTransform; use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property}; // ── Standard scale options ──────────────────────────────────────────────── const STANDARD_SCALES: &[(&str, f64)] = &[ ("1:500", 0.002), ("1:200", 0.005), ("1:100", 0.01), ("1:50", 0.02), ("1:20", 0.05), ("1:10", 0.1), ("1:5", 0.2), ("1:2", 0.5), ("1:1", 1.0), ("2:1", 2.0), ("5:1", 5.0), ("10:1", 10.0), ]; fn scale_label(scale: f64) -> String { for (label, val) in STANDARD_SCALES { if (scale - val).abs() < val * 0.01 { return label.to_string(); } } format!("{:.6}", scale).trim_end_matches('0').trim_end_matches('.').to_string() } // ── Render mode options ─────────────────────────────────────────────────── const RENDER_MODES: &[(&str, ViewportRenderMode)] = &[ ("2D Wireframe", ViewportRenderMode::Wireframe2D), ("3D Wireframe", ViewportRenderMode::Wireframe3D), ("Hidden Line", ViewportRenderMode::HiddenLine), ("Flat Shaded", ViewportRenderMode::FlatShaded), ("Gouraud Shaded", ViewportRenderMode::GouraudShaded), ("Flat Shaded + Edges", ViewportRenderMode::FlatShadedWithEdges), ("Gouraud Shaded + Edges", ViewportRenderMode::GouraudShadedWithEdges), ]; fn render_mode_label(mode: &ViewportRenderMode) -> &'static str { for (label, m) in RENDER_MODES { if m == mode { return label; } } "2D Wireframe" } // ── Shade plot mode labels ──────────────────────────────────────────────── const SHADE_PLOT_LABELS: &[&str] = &["As Displayed", "Wireframe", "Hidden", "Rendered"]; fn shade_plot_label(mode: i16) -> &'static str { SHADE_PLOT_LABELS.get(mode as usize).copied().unwrap_or("As Displayed") } // ── Standard view options ───────────────────────────────────────────────── const STD_VIEWS: &[&str] = &[ "Top", "Bottom", "Front", "Back", "Left", "Right", "SW Isometric", "SE Isometric", "NE Isometric", "NW Isometric", ]; fn grips(vp: &Viewport) -> Vec { let cx = vp.center.x as f32; let cy = vp.center.y as f32; let cz = vp.center.z as f32; let hw = (vp.width / 2.0) as f32; let hh = (vp.height / 2.0) as f32; vec![ diamond_grip(0, Vec3::new(cx, cy, cz)), square_grip(1, Vec3::new(cx + hw, cy + hh, cz)), square_grip(2, Vec3::new(cx - hw, cy + hh, cz)), square_grip(3, Vec3::new(cx - hw, cy - hh, cz)), square_grip(4, Vec3::new(cx + hw, cy - hh, cz)), ] } fn properties(vp: &Viewport) -> PropSection { let scale_opts: Vec = STANDARD_SCALES.iter().map(|(s, _)| s.to_string()).collect(); let current_scale_label = scale_label(vp.custom_scale); let view_opts: Vec = STD_VIEWS.iter().map(|s| s.to_string()).collect(); let current_view = viewport_view_label(vp); let render_opts: Vec = RENDER_MODES.iter().map(|(s, _)| s.to_string()).collect(); let current_render = render_mode_label(&vp.render_mode).to_string(); let shade_opts: Vec = SHADE_PLOT_LABELS.iter().map(|s| s.to_string()).collect(); let current_shade = shade_plot_label(vp.shade_plot_mode).to_string(); PropSection { title: "Geometry".into(), props: vec![ edit("Center X", "center_x", vp.center.x), edit("Center Y", "center_y", vp.center.y), edit("Center Z", "center_z", vp.center.z), edit("Width", "vp_w", vp.width), edit("Height", "vp_h", vp.height), // Numeric scale entry. edit("Scale (num)", "vscale", vp.custom_scale), // Standard scale picker. Property { label: "Scale".into(), field: "vscale_std", value: PropValue::Choice { selected: current_scale_label, options: scale_opts, }, }, // Standard view picker. Property { label: "View".into(), field: "vp_view", value: PropValue::Choice { selected: current_view, options: view_opts, }, }, // Render mode picker. Property { label: "Render Mode".into(), field: "vp_render", value: PropValue::Choice { selected: current_render, options: render_opts, }, }, // Shade plot mode picker. Property { label: "Shade Plot".into(), field: "vp_shade_plot", value: PropValue::Choice { selected: current_shade, options: shade_opts, }, }, // Display state toggles. Property { label: "Locked".into(), field: "vp_locked", value: PropValue::BoolToggle { field: "vp_locked", value: vp.status.locked }, }, Property { label: "On".into(), field: "vp_on", value: PropValue::BoolToggle { field: "vp_on", value: vp.status.is_on }, }, Property { label: "Perspective".into(), field: "vp_perspective", value: PropValue::BoolToggle { field: "vp_perspective", value: vp.status.perspective }, }, Property { label: "Hide Plot".into(), field: "vp_hide_plot", value: PropValue::BoolToggle { field: "vp_hide_plot", value: vp.status.hide_plot }, }, Property { label: "UCS Icon".into(), field: "vp_ucs_icon", value: PropValue::BoolToggle { field: "vp_ucs_icon", value: vp.ucs_icon_visible }, }, edit("Target X", "vtgt_x", vp.view_target.x), edit("Target Z", "vtgt_z", vp.view_target.z), // Perspective lens length. edit("Lens Length (mm)", "vp_lens", vp.lens_length), // Per-Viewport UCS. Property { label: "UCS Per Viewport".into(), field: "vp_ucs_per_vp", value: PropValue::BoolToggle { field: "vp_ucs_per_vp", value: vp.ucs_per_viewport }, }, edit("UCS Origin X", "vp_ucs_ox", vp.ucs_origin.x), edit("UCS Origin Y", "vp_ucs_oy", vp.ucs_origin.y), edit("UCS Origin Z", "vp_ucs_oz", vp.ucs_origin.z), edit("UCS X-Axis X", "vp_ucs_xx", vp.ucs_x_axis.x), edit("UCS X-Axis Y", "vp_ucs_xy", vp.ucs_x_axis.y), edit("UCS X-Axis Z", "vp_ucs_xz", vp.ucs_x_axis.z), edit("UCS Y-Axis X", "vp_ucs_yx", vp.ucs_y_axis.x), edit("UCS Y-Axis Y", "vp_ucs_yy", vp.ucs_y_axis.y), edit("UCS Y-Axis Z", "vp_ucs_yz", vp.ucs_y_axis.z), ], } } /// Identify which standard view matches the viewport's view direction. fn viewport_view_label(vp: &Viewport) -> String { let d = &vp.view_direction; let dx = d.x; let dy = d.y; let dz = d.z; // Use a simple threshold comparison to classify the view direction. if dx.abs() < 0.1 && dy.abs() < 0.1 && dz > 0.5 { return "Top".into(); } if dx.abs() < 0.1 && dy.abs() < 0.1 && dz < -0.5 { return "Bottom".into(); } if dx.abs() < 0.1 && dy < -0.5 && dz.abs() < 0.1 { return "Front".into(); } if dx.abs() < 0.1 && dy > 0.5 && dz.abs() < 0.1 { return "Back".into(); } if dx < -0.5 && dy.abs() < 0.1 && dz.abs() < 0.1 { return "Left".into(); } if dx > 0.5 && dy.abs() < 0.1 && dz.abs() < 0.1 { return "Right".into(); } if dx < -0.4 && dy < -0.4 && dz > 0.4 { return "SW Isometric".into(); } if dx > 0.4 && dy < -0.4 && dz > 0.4 { return "SE Isometric".into(); } if dx > 0.4 && dy > 0.4 && dz > 0.4 { return "NE Isometric".into(); } if dx < -0.4 && dy > 0.4 && dz > 0.4 { return "NW Isometric".into(); } "Custom".into() } fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) { use acadrust::types::Vector3; // Boolean / toggle fields handled first (value = "toggle" or "true"/"false"). match field { "vp_locked" => { vp.status.locked = if value == "toggle" { !vp.status.locked } else { value == "true" }; return; } "vp_on" => { vp.status.is_on = if value == "toggle" { !vp.status.is_on } else { value == "true" }; return; } "vp_perspective" => { vp.status.perspective = if value == "toggle" { !vp.status.perspective } else { value == "true" }; return; } "vp_hide_plot" => { vp.status.hide_plot = if value == "toggle" { !vp.status.hide_plot } else { value == "true" }; return; } "vp_ucs_icon" => { vp.ucs_icon_visible = if value == "toggle" { !vp.ucs_icon_visible } else { value == "true" }; return; } "vp_ucs_per_vp" => { vp.ucs_per_viewport = if value == "toggle" { !vp.ucs_per_viewport } else { value == "true" }; return; } _ => {} } // Standard scale picker. if field == "vscale_std" { if let Some(&(_, scale)) = STANDARD_SCALES.iter().find(|(label, _)| *label == value) { vp.custom_scale = scale; if scale > 1e-9 { vp.view_height = vp.height / scale; } } return; } // Render mode picker. if field == "vp_render" { if let Some(&(_, mode)) = RENDER_MODES.iter().find(|(label, _)| *label == value) { vp.render_mode = mode; } return; } // Shade plot mode picker. if field == "vp_shade_plot" { if let Some(idx) = SHADE_PLOT_LABELS.iter().position(|&s| s == value) { vp.shade_plot_mode = idx as i16; } return; } // Standard view direction picker. if field == "vp_view" { let dir: Option<(f64, f64, f64)> = match value { "Top" => Some(( 0.0, 0.0, 1.0)), "Bottom" => Some(( 0.0, 0.0, -1.0)), "Front" => Some(( 0.0, -1.0, 0.0)), "Back" => Some(( 0.0, 1.0, 0.0)), "Left" => Some((-1.0, 0.0, 0.0)), "Right" => Some(( 1.0, 0.0, 0.0)), "SW Isometric" => Some((-1.0, -1.0, 1.0)), "SE Isometric" => Some(( 1.0, -1.0, 1.0)), "NE Isometric" => Some(( 1.0, 1.0, 1.0)), "NW Isometric" => Some((-1.0, 1.0, 1.0)), _ => None, }; if let Some((dx, dy, dz)) = dir { let len = (dx * dx + dy * dy + dz * dz).sqrt(); vp.view_direction = Vector3::new(dx / len, dy / len, dz / len); } return; } // Numeric fields. let Some(v) = parse_f64(value) else { return }; match field { "center_x" => vp.center.x = v, "center_y" => vp.center.y = v, "center_z" => vp.center.z = v, "vp_w" if v > 0.0 => vp.width = v, "vp_h" if v > 0.0 => vp.height = v, "vscale" if v > 0.0 => { vp.custom_scale = v; vp.view_height = vp.height / v; } "vtgt_x" => vp.view_target.x = v, "vtgt_z" => vp.view_target.z = v, "vp_lens" if v > 0.0 => vp.lens_length = v, "vp_ucs_ox" => vp.ucs_origin.x = v, "vp_ucs_oy" => vp.ucs_origin.y = v, "vp_ucs_oz" => vp.ucs_origin.z = v, "vp_ucs_xx" => vp.ucs_x_axis.x = v, "vp_ucs_xy" => vp.ucs_x_axis.y = v, "vp_ucs_xz" => vp.ucs_x_axis.z = v, "vp_ucs_yx" => vp.ucs_y_axis.x = v, "vp_ucs_yy" => vp.ucs_y_axis.y = v, "vp_ucs_yz" => vp.ucs_y_axis.z = v, _ => {} } } fn apply_grip(vp: &mut Viewport, grip_id: usize, apply: GripApply) { match (grip_id, apply) { (0, GripApply::Translate(d)) => { vp.center.x += d.x as f64; vp.center.y += d.y as f64; vp.center.z += d.z as f64; } (0, GripApply::Absolute(p)) => { vp.center.x = p.x as f64; vp.center.y = p.y as f64; vp.center.z = p.z as f64; } (1..=4, GripApply::Absolute(p)) => { let new_hw = (p.x as f64 - vp.center.x).abs(); let new_hh = (p.y as f64 - vp.center.y).abs(); if new_hw > 0.01 { vp.width = new_hw * 2.0; } if new_hh > 0.01 { let new_h = new_hh * 2.0; // Keep scale constant: view_height must scale with the viewport height. if vp.height > 1e-9 && vp.view_height.abs() > 1e-9 { vp.view_height = vp.view_height * (new_h / vp.height); } vp.height = new_h; } } _ => {} } } fn apply_transform(vp: &mut Viewport, t: &EntityTransform) { crate::scene::transform::apply_standard_entity_transform(vp, t, |entity, p1, p2| { crate::scene::transform::reflect_xy_point( &mut entity.center.x, &mut entity.center.y, p1, p2, ); }); } impl Grippable for Viewport { fn grips(&self) -> Vec { grips(self) } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { apply_grip(self, grip_id, apply); } } impl PropertyEditable for Viewport { fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection { properties(self) } fn apply_geom_prop(&mut self, field: &str, value: &str) { apply_geom_prop(self, field, value); } } impl Transformable for Viewport { fn apply_transform(&mut self, t: &EntityTransform) { apply_transform(self, t); } }