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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-03-31 07:27:03 +03:00
1 parent 523ba74c10
commit 031d2311ed
3 files changed
+174 -7

No files matched your search

+20 -1
View File
@@ -195,6 +195,25 @@ impl H7CAD {
if matches!(&entity, acadrust::EntityType::Viewport(_))
&& 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();
match self.tabs[i]
.scene
@@ -204,7 +223,7 @@ impl H7CAD {
Ok(_) => {}
Err(e) => self
.command_line
.push_error(&format!("Viewport eklenemedi: {e}")),
.push_error(&format!("Viewport could not be added: {e}")),
}
} else {
self.tabs[i].scene.add_entity(entity);
+145 -5
View File
@@ -4,7 +4,40 @@ 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};
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> {
let cx = vp.center.x as f32;
@@ -22,6 +55,12 @@ fn grips(vp: &Viewport) -> Vec<GripDef> {
}
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 {
title: "Geometry".into(),
props: vec![
@@ -30,24 +69,125 @@ fn properties(vp: &Viewport) -> PropSection {
edit("Center Z", "center_z", vp.center.z),
edit("Width", "vp_w", vp.width),
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 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) {
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;
}
"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 {
"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,
"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,
_ => {}
+9 -1
View File
@@ -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`.
fn current_layout_block_handle(&self) -> Handle {
self.document
@@ -382,7 +389,8 @@ impl Scene {
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();
if viewports.is_empty() {