Phase 4: Viewport control and properties
- Viewport ID uniqueness: commit_entity assigns max_existing_id+1 instead of always using id=2 - Lock/On boolean toggles in Properties panel (vp_locked, vp_on) backed by status.locked / status.is_on fields - Standard scale Choice picker: 1:500 to 10:1 (also syncs view_height) - Numeric scale edit now syncs view_height when changed - Standard view Choice picker: Top/Bottom/Front/Back/Left/Right/4×Iso updates view_direction vector via apply_geom_prop - Viewport content not rendered when status.is_on is false - current_layout_block_handle_pub() added for external access Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
523ba74c10
commit
031d2311ed
3 changed files with 174 additions and 7 deletions
|
|
@ -195,6 +195,25 @@ impl H7CAD {
|
||||||
if matches!(&entity, acadrust::EntityType::Viewport(_))
|
if matches!(&entity, acadrust::EntityType::Viewport(_))
|
||||||
&& self.tabs[i].scene.current_layout != "Model"
|
&& self.tabs[i].scene.current_layout != "Model"
|
||||||
{
|
{
|
||||||
|
// Assign a unique viewport ID (max existing id + 1, min 2).
|
||||||
|
if let acadrust::EntityType::Viewport(ref mut vp) = entity {
|
||||||
|
let layout_block = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let max_id = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|e| {
|
||||||
|
if let acadrust::EntityType::Viewport(v) = e {
|
||||||
|
if v.common.owner_handle == layout_block { Some(v.id) } else { None }
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(1);
|
||||||
|
vp.id = (max_id + 1).max(2);
|
||||||
|
}
|
||||||
|
|
||||||
let layout = self.tabs[i].scene.current_layout.clone();
|
let layout = self.tabs[i].scene.current_layout.clone();
|
||||||
match self.tabs[i]
|
match self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -204,7 +223,7 @@ impl H7CAD {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => self
|
Err(e) => self
|
||||||
.command_line
|
.command_line
|
||||||
.push_error(&format!("Viewport eklenemedi: {e}")),
|
.push_error(&format!("Viewport could not be added: {e}")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].scene.add_entity(entity);
|
self.tabs[i].scene.add_entity(entity);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,40 @@ use glam::Vec3;
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip};
|
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, square_grip};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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<GripDef> {
|
fn grips(vp: &Viewport) -> Vec<GripDef> {
|
||||||
let cx = vp.center.x as f32;
|
let cx = vp.center.x as f32;
|
||||||
|
|
@ -22,6 +55,12 @@ fn grips(vp: &Viewport) -> Vec<GripDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(vp: &Viewport) -> PropSection {
|
fn properties(vp: &Viewport) -> PropSection {
|
||||||
|
let scale_opts: Vec<String> = STANDARD_SCALES.iter().map(|(s, _)| s.to_string()).collect();
|
||||||
|
let current_scale_label = scale_label(vp.custom_scale);
|
||||||
|
|
||||||
|
let view_opts: Vec<String> = STD_VIEWS.iter().map(|s| s.to_string()).collect();
|
||||||
|
let current_view = viewport_view_label(vp);
|
||||||
|
|
||||||
PropSection {
|
PropSection {
|
||||||
title: "Geometry".into(),
|
title: "Geometry".into(),
|
||||||
props: vec![
|
props: vec![
|
||||||
|
|
@ -30,24 +69,125 @@ fn properties(vp: &Viewport) -> PropSection {
|
||||||
edit("Center Z", "center_z", vp.center.z),
|
edit("Center Z", "center_z", vp.center.z),
|
||||||
edit("Width", "vp_w", vp.width),
|
edit("Width", "vp_w", vp.width),
|
||||||
edit("Height", "vp_h", vp.height),
|
edit("Height", "vp_h", vp.height),
|
||||||
edit("Scale", "vscale", vp.custom_scale),
|
// 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 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 },
|
||||||
|
},
|
||||||
edit("Target X", "vtgt_x", vp.view_target.x),
|
edit("Target X", "vtgt_x", vp.view_target.x),
|
||||||
edit("Target Z", "vtgt_z", vp.view_target.z),
|
edit("Target Z", "vtgt_z", vp.view_target.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) {
|
fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
||||||
let Some(v) = parse_f64(value) else {
|
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;
|
return;
|
||||||
|
}
|
||||||
|
"vp_on" => {
|
||||||
|
vp.status.is_on = if value == "toggle" { !vp.status.is_on } 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
match field {
|
||||||
"center_x" => vp.center.x = v,
|
"center_x" => vp.center.x = v,
|
||||||
"center_y" => vp.center.y = v,
|
"center_y" => vp.center.y = v,
|
||||||
"center_z" => vp.center.z = v,
|
"center_z" => vp.center.z = v,
|
||||||
"vp_w" if v > 0.0 => vp.width = v,
|
"vp_w" if v > 0.0 => vp.width = v,
|
||||||
"vp_h" if v > 0.0 => vp.height = v,
|
"vp_h" if v > 0.0 => vp.height = v,
|
||||||
"vscale" if v > 0.0 => vp.custom_scale = v,
|
"vscale" if v > 0.0 => {
|
||||||
|
vp.custom_scale = v;
|
||||||
|
vp.view_height = vp.height / v;
|
||||||
|
}
|
||||||
"vtgt_x" => vp.view_target.x = v,
|
"vtgt_x" => vp.view_target.x = v,
|
||||||
"vtgt_z" => vp.view_target.z = v,
|
"vtgt_z" => vp.view_target.z = v,
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,13 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Public accessor for the block-record handle of the current layout.
|
||||||
|
/// Used by external callers (e.g. `commit_entity`) that need the handle
|
||||||
|
/// without going through private API.
|
||||||
|
pub fn current_layout_block_handle_pub(&self) -> Handle {
|
||||||
|
self.current_layout_block_handle()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the block-record handle for `current_layout`.
|
/// Returns the block-record handle for `current_layout`.
|
||||||
fn current_layout_block_handle(&self) -> Handle {
|
fn current_layout_block_handle(&self) -> Handle {
|
||||||
self.document
|
self.document
|
||||||
|
|
@ -382,7 +389,8 @@ impl Scene {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(|vp| vp.id > 1 && vp.common.owner_handle == paper_block)
|
// Only user viewports (id > 1) that belong to this layout AND are turned on.
|
||||||
|
.filter(|vp| vp.id > 1 && vp.common.owner_handle == paper_block && vp.status.is_on)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if viewports.is_empty() {
|
if viewports.is_empty() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue