Feat: viewport display lock, extended properties, VPLAYER and VPORTS commands
- Enforce status.locked in pan_active_viewport and zoom_active_viewport: locked viewports now reject pan/zoom inputs entirely - Add perspective, render_mode, shade_plot_mode, hide_plot, ucs_icon_visible fields to the viewport property panel with appropriate pickers/toggles - Add VPORTS command: lists all user viewports in the current layout (id, size, position, scale, on/locked state) in the command line - Add VPLAYER command: per-viewport layer freeze/thaw from the command line (F <layer> to freeze, T <layer> to thaw, Enter to exit) - Add CmdResult::VpLayerUpdate variant and its handler in apply_cmd_result Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b013619c25
commit
73b224c191
7 changed files with 311 additions and 1 deletions
|
|
@ -285,6 +285,59 @@ impl H7CAD {
|
|||
self.command_line.push_info("No groups found for selected objects.");
|
||||
}
|
||||
}
|
||||
CmdResult::VpLayerUpdate { vp_handle, freeze, thaw } => {
|
||||
// Resolve layer names → handles, then update frozen_layers on the viewport.
|
||||
let freeze_handles: Vec<Handle> = freeze.iter()
|
||||
.filter_map(|name| {
|
||||
self.tabs[i].scene.document.layers.iter()
|
||||
.find(|l| l.name.eq_ignore_ascii_case(name))
|
||||
.map(|l| l.handle)
|
||||
})
|
||||
.collect();
|
||||
let thaw_handles: Vec<Handle> = thaw.iter()
|
||||
.filter_map(|name| {
|
||||
self.tabs[i].scene.document.layers.iter()
|
||||
.find(|l| l.name.eq_ignore_ascii_case(name))
|
||||
.map(|l| l.handle)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut frozen_count = 0usize;
|
||||
let mut thawed_count = 0usize;
|
||||
|
||||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(vp_handle)
|
||||
{
|
||||
for h in &freeze_handles {
|
||||
if !vp.frozen_layers.contains(h) {
|
||||
vp.frozen_layers.push(*h);
|
||||
frozen_count += 1;
|
||||
}
|
||||
}
|
||||
for h in &thaw_handles {
|
||||
let before = vp.frozen_layers.len();
|
||||
vp.frozen_layers.retain(|fh| fh != h);
|
||||
if vp.frozen_layers.len() < before { thawed_count += 1; }
|
||||
}
|
||||
}
|
||||
|
||||
if frozen_count > 0 || thawed_count > 0 {
|
||||
self.push_undo_snapshot(i, "VPLAYER");
|
||||
self.tabs[i].dirty = true;
|
||||
if frozen_count > 0 {
|
||||
self.command_line.push_info(&format!("VPLAYER: {frozen_count} layer(s) frozen in viewport."));
|
||||
}
|
||||
if thawed_count > 0 {
|
||||
self.command_line.push_info(&format!("VPLAYER: {thawed_count} layer(s) thawed in viewport."));
|
||||
}
|
||||
}
|
||||
|
||||
// Show updated prompt (command stays active for more operations).
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Focus the command-line input while a command is active; blur it when the command ends.
|
||||
if self.tabs[i].active_cmd.is_some() {
|
||||
|
|
|
|||
|
|
@ -909,6 +909,75 @@ impl H7CAD {
|
|||
return Task::done(Message::PspaceCommand);
|
||||
}
|
||||
|
||||
// ── VPORTS — list viewports in current layout ─────────────────
|
||||
"VPORTS" => {
|
||||
let scene = &self.tabs[i].scene;
|
||||
if scene.current_layout == "Model" {
|
||||
self.command_line.push_error("VPORTS: switch to a paper space layout first.");
|
||||
} else {
|
||||
let layout_block = scene.current_layout_block_handle_pub();
|
||||
let viewports: Vec<_> = scene.document.entities()
|
||||
.filter_map(|e| {
|
||||
if let acadrust::EntityType::Viewport(vp) = e {
|
||||
if vp.id > 1 && vp.common.owner_handle == layout_block {
|
||||
Some((vp.id, vp.center.clone(), vp.width, vp.height, vp.custom_scale, vp.status.is_on, vp.status.locked))
|
||||
} else { None }
|
||||
} else { None }
|
||||
})
|
||||
.collect();
|
||||
if viewports.is_empty() {
|
||||
self.command_line.push_info("No viewports in current layout. Use MVIEW to create one.");
|
||||
} else {
|
||||
self.command_line.push_output(&format!("{} viewport(s) in layout \"{}\":", viewports.len(), scene.current_layout));
|
||||
for (id, center, w, h, scale, is_on, locked) in &viewports {
|
||||
let state = match (is_on, locked) {
|
||||
(true, true) => "On, Locked",
|
||||
(true, false) => "On",
|
||||
(false, _) => "Off",
|
||||
};
|
||||
self.command_line.push_output(&format!(
|
||||
" VP #{id}: {w:.1}×{h:.1} @ ({:.1},{:.1}) scale={scale:.4} [{state}]",
|
||||
center.x, center.y
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── VPLAYER — per-viewport layer freeze/thaw ──────────────────
|
||||
"VPLAYER" => {
|
||||
let scene = &self.tabs[i].scene;
|
||||
if scene.current_layout == "Model" {
|
||||
self.command_line.push_error("VPLAYER: switch to a paper space layout first.");
|
||||
} else if scene.active_viewport.is_none() {
|
||||
self.command_line.push_error("VPLAYER: enter a viewport first (double-click or MS).");
|
||||
} else {
|
||||
use crate::modules::layout::vplayer::VplayerCommand;
|
||||
let vp_handle = scene.active_viewport.unwrap();
|
||||
// Collect current frozen layer names for display.
|
||||
let frozen_names: Vec<String> = {
|
||||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||
scene.document.get_entity(vp_handle)
|
||||
{
|
||||
vp.frozen_layers.iter().filter_map(|h| {
|
||||
scene.document.layers.iter().find(|l| l.handle == *h).map(|l| l.name.clone())
|
||||
}).collect()
|
||||
} else { vec![] }
|
||||
};
|
||||
if frozen_names.is_empty() {
|
||||
self.command_line.push_info("VPLAYER: no frozen layers in active viewport.");
|
||||
} else {
|
||||
self.command_line.push_info(&format!(
|
||||
"VPLAYER: frozen layers: {}",
|
||||
frozen_names.join(", ")
|
||||
));
|
||||
}
|
||||
let new_cmd = VplayerCommand::new(vp_handle);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Plot / Page Setup ──────────────────────────────────────────
|
||||
"PRINT"|"PLOT"|"EXPORT" => {
|
||||
return Task::done(Message::PlotExport);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ pub enum CmdResult {
|
|||
CreateGroup { handles: Vec<Handle>, name: String },
|
||||
/// Dissolve all groups that contain any of the given handles; end command.
|
||||
DeleteGroups { handles: Vec<Handle> },
|
||||
/// Freeze or thaw layers by name in the given viewport; command stays active.
|
||||
VpLayerUpdate {
|
||||
vp_handle: Handle,
|
||||
freeze: Vec<String>,
|
||||
thaw: Vec<String>,
|
||||
},
|
||||
/// Paste clipboard entities translated so their centroid lands at `base_pt`; end command.
|
||||
PasteClipboard { base_pt: Vec3 },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use acadrust::entities::Viewport;
|
||||
use acadrust::entities::{Viewport, ViewportRenderMode};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
|
|
@ -32,6 +32,33 @@ fn scale_label(scale: f64) -> 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] = &[
|
||||
|
|
@ -61,6 +88,12 @@ fn properties(vp: &Viewport) -> PropSection {
|
|||
let view_opts: Vec<String> = STD_VIEWS.iter().map(|s| s.to_string()).collect();
|
||||
let current_view = viewport_view_label(vp);
|
||||
|
||||
let render_opts: Vec<String> = RENDER_MODES.iter().map(|(s, _)| s.to_string()).collect();
|
||||
let current_render = render_mode_label(&vp.render_mode).to_string();
|
||||
|
||||
let shade_opts: Vec<String> = 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![
|
||||
|
|
@ -89,6 +122,24 @@ fn properties(vp: &Viewport) -> PropSection {
|
|||
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(),
|
||||
|
|
@ -100,6 +151,21 @@ fn properties(vp: &Viewport) -> PropSection {
|
|||
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),
|
||||
],
|
||||
|
|
@ -140,6 +206,18 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
|||
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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
@ -154,6 +232,22 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// This tab is only shown when the active layout is not "Model".
|
||||
|
||||
pub mod mview;
|
||||
pub mod vplayer;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, ToolDef};
|
||||
|
||||
|
|
|
|||
85
src/modules/layout/vplayer.rs
Normal file
85
src/modules/layout/vplayer.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// VPLAYER — per-viewport layer freeze/thaw command.
|
||||
//
|
||||
// Usage (command line):
|
||||
// VPLAYER
|
||||
// > F <layer_name> → freeze layer in active viewport
|
||||
// > T <layer_name> → thaw layer in active viewport
|
||||
// > Enter → exit
|
||||
//
|
||||
// Layer names are case-insensitive. Multiple space-separated names are accepted.
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
pub struct VplayerCommand {
|
||||
vp_handle: Handle,
|
||||
}
|
||||
|
||||
impl VplayerCommand {
|
||||
pub fn new(vp_handle: Handle) -> Self {
|
||||
Self { vp_handle }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for VplayerCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"VPLAYER"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"VPLAYER F <layer> = Freeze | T <layer> = Thaw | Enter = Exit".to_string()
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let text = text.trim();
|
||||
if text.is_empty() {
|
||||
return Some(CmdResult::Cancel);
|
||||
}
|
||||
|
||||
let mut parts = text.splitn(2, char::is_whitespace);
|
||||
let op = parts.next().unwrap_or("").to_uppercase();
|
||||
let rest = parts.next().unwrap_or("").trim();
|
||||
|
||||
let layer_names: Vec<String> = rest
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
if layer_names.is_empty() {
|
||||
return None; // no layer name given — ignore and re-prompt
|
||||
}
|
||||
|
||||
match op.as_str() {
|
||||
"F" | "FREEZE" => Some(CmdResult::VpLayerUpdate {
|
||||
vp_handle: self.vp_handle,
|
||||
freeze: layer_names,
|
||||
thaw: vec![],
|
||||
}),
|
||||
"T" | "THAW" => Some(CmdResult::VpLayerUpdate {
|
||||
vp_handle: self.vp_handle,
|
||||
freeze: vec![],
|
||||
thaw: layer_names,
|
||||
}),
|
||||
_ => None, // unknown op — ignore
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, _pt: Vec3) -> Option<WireModel> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -767,6 +767,7 @@ impl Scene {
|
|||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||
self.document.get_entity_mut(vp_handle)
|
||||
{
|
||||
if vp.status.locked { return; }
|
||||
let scale = if vp.custom_scale.abs() > 1e-9 {
|
||||
vp.custom_scale
|
||||
} else if vp.view_height.abs() > 1e-9 {
|
||||
|
|
@ -796,6 +797,7 @@ impl Scene {
|
|||
if let Some(acadrust::EntityType::Viewport(vp)) =
|
||||
self.document.get_entity_mut(vp_handle)
|
||||
{
|
||||
if vp.status.locked { return; }
|
||||
// Zoom in = shrink view_height → higher scale → objects appear larger.
|
||||
let factor = (1.0_f64 - 0.15 * steps as f64).clamp(0.1, 10.0);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue