forked from 1b-agent/opencadstudio-upstream
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:
1 parent
523ba74c10
commit
031d2311ed
3 files changed
+174
-7
No files matched your search
+20
-1
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user