forked from 1b-agent/opencadstudio-upstream
fix: open-issue sweep — drawing, plot, snap, layout and UI fixes
Fixes: - Font prompt no longer lists bundled ltypeshp/qcadshp shape files (#1626) - Wide polylines step at a discontinuous segment width; PEDIT marks the current vertex; keyword steps refresh the command preview (#1628) - Radius-0 fillet drops the zero-length segment it left, so the corner fillets again (#1422) - FILLET Trim / No trim option (#1558) - True-colour wires plot in their own colour; colour picker stores the value it shows (#1621) - Model tab keeps its page setup; "Save changes to layout" in Model (#1620) - QNEW default template in Options > Files; .dwt in the open dialog (#1134, #1113) - GRIDUNIT / GRIDMODE / PERSPECTIVE variables (#1618, #1524) - PTYPE (#1433) - ZOOM nX / nXP (#1465) - CHSPACE between model and paper space through a viewport (#1428) - Moving a dimension keeps its automatic text placement - Shell icon; "&&" button labels (#1554) - LINETYPE Load from a .lin file (#1588) - XLINE / RAY hide the crosshair arms (#1437) - Dynamic-input boxes keep clear of the crosshair and each other (#1546) - Snap marker colour setting, dark default on light canvases (#1497, #1229, #1128) - Dimension style precision as a 0.00 pattern dropdown (#1426) - Node graph nodes left of the origin no longer clip away (#1592) - Collapse button in the command history header (#1094) - Freeze-in-current-viewport toggle in the ribbon layer list (#1466) - Geometric Center object snap for closed polylines (#1369) New strings translated in all 21 locales. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
abf302eba5
commit
178c7dff41
70 files changed
+1227
-145
No files matched your search
@@ -677,6 +677,7 @@ impl OpenCADStudio {
|
||||
None
|
||||
};
|
||||
let is_escape = matches!(&input, StepInput::Escape);
|
||||
let keyed = matches!(&input, StepInput::Text(_) | StepInput::Enter);
|
||||
let result: Option<CmdResult> = {
|
||||
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
||||
return (Task::none(), false);
|
||||
@@ -703,7 +704,15 @@ impl OpenCADStudio {
|
||||
self.tabs[i].pending_pause_tokens = None;
|
||||
}
|
||||
match result {
|
||||
Some(r) => (self.apply_cmd_result(r), true),
|
||||
Some(r) => {
|
||||
let task = self.apply_cmd_result(r);
|
||||
// A keyword can move the step without the cursor moving
|
||||
// (PEDIT Next vertex): redraw its preview now (#1628).
|
||||
if keyed && self.active_tab == i {
|
||||
self.refresh_active_cmd_preview(i);
|
||||
}
|
||||
(task, true)
|
||||
}
|
||||
None => (Task::none(), false),
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -911,13 +911,29 @@ impl OpenCADStudio {
|
||||
.or_else(|| cmd.strip_prefix("ZOOM S "))
|
||||
.or_else(|| cmd.strip_prefix("ZS "))
|
||||
.unwrap_or("1");
|
||||
if let Ok(factor) = rest.trim().parse::<f32>() {
|
||||
if factor > 0.0 {
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
self.tabs[i].scene.zoom_camera(1.0 / factor);
|
||||
// nXP scales the active viewport against paper space, nX (or
|
||||
// a bare n) the current view (#1465). n may be an expression
|
||||
// such as 1/50.
|
||||
let rest = rest.trim().to_ascii_uppercase();
|
||||
let (number, xp) = match rest.strip_suffix("XP") {
|
||||
Some(n) => (n, true),
|
||||
None => (rest.strip_suffix('X').unwrap_or(&rest), false),
|
||||
};
|
||||
let factor = crate::app::expr_eval::eval_number(number.trim())
|
||||
.filter(|f| f.is_finite() && *f > 0.0);
|
||||
match factor {
|
||||
Some(factor) if xp && self.tabs[i].scene.set_viewport_scale_factor(factor) => {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(crate::tf!("Zoom Scale ×{factor:.3}").as_ref());
|
||||
}
|
||||
Some(factor) => {
|
||||
self.tabs[i].scene.remember_current_view();
|
||||
self.tabs[i].scene.zoom_camera(1.0 / factor as f32);
|
||||
self.command_line
|
||||
.push_output(crate::tf!("Zoom Scale ×{factor:.3}").as_ref());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -634,6 +634,9 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"FITSPLINE",
|
||||
// System variables (typeable directly).
|
||||
"MIRRTEXT",
|
||||
"PERSPECTIVE",
|
||||
"GRIDMODE",
|
||||
"GRIDUNIT",
|
||||
"ZOOMWHEEL",
|
||||
"ZOOMFACTOR",
|
||||
"SHORTCUTMENU",
|
||||
@@ -844,6 +847,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"MLSTYLE",
|
||||
"MS",
|
||||
"MSPACE",
|
||||
"CHSPACE",
|
||||
"NAVVCUBE",
|
||||
"NEW",
|
||||
"OBJIMPORT",
|
||||
|
||||
@@ -3,6 +3,65 @@ use super::*;
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
// View / grid variables held by the app rather than the header,
|
||||
// typed bare or through SETVAR (#1524, #1618).
|
||||
cmd if matches!(
|
||||
cmd.strip_prefix("SETVAR ").unwrap_or(cmd).split_whitespace().next(),
|
||||
Some("PERSPECTIVE" | "GRIDMODE" | "GRIDUNIT")
|
||||
) =>
|
||||
{
|
||||
let mut words = cmd.strip_prefix("SETVAR ").unwrap_or(cmd).splitn(2, ' ');
|
||||
let name = words.next().unwrap_or_default().to_string();
|
||||
let value = words.next().map(str::trim).filter(|v| !v.is_empty());
|
||||
let current = match name.as_str() {
|
||||
"PERSPECTIVE" => {
|
||||
let persp = self.tabs[i].scene.camera.borrow().projection
|
||||
== crate::scene::Projection::Perspective;
|
||||
(persp as i32).to_string()
|
||||
}
|
||||
"GRIDMODE" => (self.show_grid as i32).to_string(),
|
||||
_ => format!("{},{}", self.grid_spacing_x, self.grid_spacing_y),
|
||||
};
|
||||
let Some(value) = value else {
|
||||
self.command_line
|
||||
.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
|
||||
self.pending_setvar = Some(name);
|
||||
return Some(Task::none());
|
||||
};
|
||||
match (name.as_str(), value) {
|
||||
("PERSPECTIVE", "0" | "1") => {
|
||||
return Some(Task::done(Message::SetProjection(value == "0")));
|
||||
}
|
||||
("GRIDMODE", "0" | "1") => {
|
||||
if (value == "1") != self.show_grid {
|
||||
return Some(Task::done(Message::ToggleGrid));
|
||||
}
|
||||
}
|
||||
("GRIDUNIT", _) => {
|
||||
let mut parts = value.split(',').map(|p| p.trim().parse::<f32>());
|
||||
let x = parts.next().and_then(Result::ok).filter(|v| *v > 0.0);
|
||||
let y = match parts.next() {
|
||||
Some(p) => p.ok().filter(|v| *v > 0.0),
|
||||
None => x,
|
||||
};
|
||||
let (Some(x), Some(y)) = (x, y) else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Requires two positive values (x,y) or one for both.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
};
|
||||
self.grid_spacing_x = x;
|
||||
self.grid_spacing_y = y;
|
||||
self.sync_vport_display(i);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
_ => self
|
||||
.command_line
|
||||
.push_error(crate::t!("Requires 0 or 1").as_ref()),
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
"CETRANSPARENCY" => return self.dispatch_styleprops("SETVAR CETRANSPARENCY", i),
|
||||
cmd if cmd.starts_with("CETRANSPARENCY ") => {
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -84,9 +143,10 @@ impl OpenCADStudio {
|
||||
use crate::command::KeywordCommand;
|
||||
let c = KeywordCommand::new(
|
||||
"LINETYPE",
|
||||
"LINETYPE [List / Set]:",
|
||||
"LINETYPE [List / Set / Load]:",
|
||||
vec![
|
||||
("List", "LIST", None),
|
||||
("Load", "LOAD", None),
|
||||
("Set", "SET", Some("LINETYPE SET linetype name (ByLayer / ByBlock / …):")),
|
||||
],
|
||||
);
|
||||
@@ -98,6 +158,13 @@ impl OpenCADStudio {
|
||||
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
|
||||
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
|
||||
match sub.as_str() {
|
||||
// Load the definitions of a `.lin` file (#1588).
|
||||
"LOAD" | "L" => {
|
||||
return Some(Task::perform(
|
||||
crate::io::pick_linetype_file(),
|
||||
Message::LinetypeLoaded,
|
||||
));
|
||||
}
|
||||
"" | "LIST" | "?" => {
|
||||
let ltypes: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
@@ -3247,7 +3314,7 @@ impl OpenCADStudio {
|
||||
);
|
||||
}
|
||||
}
|
||||
cmd if cmd == "DDPTYPE" => {
|
||||
cmd if cmd == "DDPTYPE" || cmd == "PTYPE" => {
|
||||
// The dialog shows the magnitude; the sign (relative/absolute)
|
||||
// is driven by the radio buttons. A positive PDSIZE is absolute;
|
||||
// zero or negative is relative.
|
||||
|
||||
@@ -548,6 +548,102 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::PspaceCommand));
|
||||
}
|
||||
|
||||
// ── CHSPACE — move objects between model and paper space through
|
||||
// a viewport, keeping their apparent size and place (#1428).
|
||||
// Inside a viewport the selection goes to the sheet; on the
|
||||
// sheet it goes into the model through the viewport under it.
|
||||
"CHSPACE" => {
|
||||
let handles: Vec<codec::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
if self.tabs[i].scene.current_layout == "Model" {
|
||||
self.command_line
|
||||
.push_error(crate::t!("CHSPACE: Command not allowed in the Model tab.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let cmd = SelectObjectsCommand::new("CHSPACE");
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
return Some(Task::none());
|
||||
}
|
||||
let scene = &self.tabs[i].scene;
|
||||
let to_paper = scene.active_viewport.is_some();
|
||||
let frame = match scene.active_viewport {
|
||||
Some(vp) => scene.viewport_frame(vp),
|
||||
None => {
|
||||
// The viewport under the middle of the selection.
|
||||
let (mut lo, mut hi) = (glam::DVec2::MAX, glam::DVec2::MIN);
|
||||
for wire in scene.wire_models_for(&handles) {
|
||||
let [x0, y0, x1, y1] = wire.aabb.map(f64::from);
|
||||
if [x0, y0, x1, y1].iter().all(|v| v.is_finite()) {
|
||||
lo = lo.min(glam::DVec2::new(x0, y0));
|
||||
hi = hi.max(glam::DVec2::new(x1, y1));
|
||||
}
|
||||
}
|
||||
let mid = (lo + hi) * 0.5;
|
||||
mid.is_finite()
|
||||
.then(|| scene.viewport_frames_at_paper_point(mid.extend(0.0)))
|
||||
.and_then(|frames| frames.into_iter().next())
|
||||
}
|
||||
};
|
||||
let Some(frame) = frame else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("CHSPACE: No plan-view viewport to change space through.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
};
|
||||
use crate::command::EntityTransform as T;
|
||||
let z = glam::DVec3::Z;
|
||||
let steps = if to_paper {
|
||||
[
|
||||
T::Translate((-frame.model_target).extend(0.0)),
|
||||
T::Rotate { center: glam::DVec3::ZERO, axis: z, angle_rad: frame.twist },
|
||||
T::Scale { center: glam::DVec3::ZERO, factor: frame.scale },
|
||||
T::Translate(frame.paper_center.extend(0.0)),
|
||||
]
|
||||
} else {
|
||||
[
|
||||
T::Translate((-frame.paper_center).extend(0.0)),
|
||||
T::Scale { center: glam::DVec3::ZERO, factor: 1.0 / frame.scale },
|
||||
T::Rotate { center: glam::DVec3::ZERO, axis: z, angle_rad: -frame.twist },
|
||||
T::Translate(frame.model_target.extend(0.0)),
|
||||
]
|
||||
};
|
||||
self.push_undo_snapshot(i, "CHSPACE");
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
let entities: Vec<codec::EntityType> = handles
|
||||
.iter()
|
||||
.filter_map(|h| scene.document.get_entity(*h).cloned())
|
||||
.collect();
|
||||
scene.deselect_all();
|
||||
scene.erase_entities(&handles);
|
||||
// The clones land in the other space: the sheet with no active
|
||||
// viewport, the model with one.
|
||||
let active = scene.active_viewport;
|
||||
scene.active_viewport = if to_paper { None } else { Some(frame.viewport) };
|
||||
let moved: Vec<codec::Handle> =
|
||||
entities.into_iter().map(|e| scene.add_entity_clone(e)).collect();
|
||||
scene.active_viewport = active;
|
||||
for step in &steps {
|
||||
scene.transform_entities(&moved, step);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
let count = moved.len();
|
||||
self.command_line.push_output(
|
||||
if to_paper {
|
||||
crate::tf!("CHSPACE: {count} object(s) changed to paper space.")
|
||||
} else {
|
||||
crate::tf!("CHSPACE: {count} object(s) changed to model space.")
|
||||
}
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Viewport arrangement shortcuts ────────────────────────────
|
||||
// Tile the model viewports into preset splits. Each delegates to the
|
||||
// matching VPORTS configuration so the Model/paper handling stays in
|
||||
|
||||
@@ -88,6 +88,11 @@ impl OpenCADStudio {
|
||||
return;
|
||||
}
|
||||
let active = self.tabs[i].active_layer.clone();
|
||||
let vp_column = self.tabs[i].scene.active_viewport.and_then(|vp| {
|
||||
(self.tabs[i].scene.current_layout != "Model")
|
||||
.then(|| self.tabs[i].layers.vp_cols.iter().position(|c| c.handle == vp))
|
||||
.flatten()
|
||||
});
|
||||
let infos: Vec<crate::ui::ribbon::LayerInfo> = self.tabs[i]
|
||||
.layers
|
||||
.layers
|
||||
@@ -100,6 +105,8 @@ impl OpenCADStudio {
|
||||
visible: l.visible,
|
||||
frozen: l.frozen,
|
||||
locked: l.locked,
|
||||
vp_frozen: vp_column
|
||||
.map(|column| (column, l.vp_frozen.get(column).copied().unwrap_or(false))),
|
||||
})
|
||||
.collect();
|
||||
let names: Vec<String> = infos.iter().map(|l| l.name.clone()).collect();
|
||||
|
||||
@@ -590,6 +590,10 @@ pub(super) struct OpenCADStudio {
|
||||
crosshair_color: Option<[u8; 3]>,
|
||||
/// Editable Options buffer for the crosshair colour.
|
||||
crosshair_color_input: String,
|
||||
/// Explicit object-snap marker colour; `None` keeps the automatic one.
|
||||
snap_marker_color: Option<[u8; 3]>,
|
||||
/// Editable Options buffer for the snap marker colour.
|
||||
snap_marker_color_input: String,
|
||||
/// Defer the ISOLINES mesh rebuild until the slider is released.
|
||||
isolines_awaiting_regen: bool,
|
||||
/// Edit buffer for the SNAPANG field on the Options Drafting page. Kept
|
||||
@@ -1024,6 +1028,8 @@ pub(super) struct OpenCADStudio {
|
||||
check_missing_fonts: bool,
|
||||
/// Custom font source base URL; empty selects the community repository.
|
||||
font_source_url: String,
|
||||
/// Template a new drawing starts from (QNEW); empty = blank.
|
||||
qnew_template: String,
|
||||
/// Editable copy of `font_source_url` shown in the missing-fonts prompt.
|
||||
font_source_input: String,
|
||||
donation_prompt_version: String,
|
||||
@@ -2289,6 +2295,10 @@ pub enum Message {
|
||||
LineweightDisplayScaleChanged(i32),
|
||||
/// Edit the optional crosshair RGB value; blank restores automatic contrast.
|
||||
CrosshairColorChanged(String),
|
||||
SnapMarkerColorChanged(String),
|
||||
/// Options → Files: the default template for new drawings.
|
||||
QnewTemplateChanged(String),
|
||||
QnewTemplateBrowse,
|
||||
/// Set the default type/version used when first saving a new drawing.
|
||||
DefaultSaveFormatChanged(String),
|
||||
/// Select one of Iced's built-in themes or the editable Custom theme.
|
||||
@@ -3678,6 +3688,8 @@ pub enum Message {
|
||||
// ── Plot Style Table ─────────────────────────────────────────────────
|
||||
/// Open file dialog to load a CTB/STB plot style table.
|
||||
PlotStyleLoad,
|
||||
/// LINETYPE Load: the picked `.lin` file's text, `None` when cancelled.
|
||||
LinetypeLoaded(Option<String>),
|
||||
/// Callback when the user picks (or cancels) a CTB/STB file.
|
||||
/// The Load… picker finished: a table, nothing (cancelled), or why the
|
||||
/// file could not be read.
|
||||
@@ -4151,6 +4163,8 @@ impl OpenCADStudio {
|
||||
cursor_type: settings::CursorType::Crosshair,
|
||||
crosshair_color: None,
|
||||
crosshair_color_input: String::new(),
|
||||
snap_marker_color: None,
|
||||
snap_marker_color_input: String::new(),
|
||||
isolines_awaiting_regen: false,
|
||||
snap_angle_input: "0".to_string(),
|
||||
lineweight_display_scale: 100,
|
||||
@@ -4338,6 +4352,7 @@ impl OpenCADStudio {
|
||||
default_assoc_prompted: false,
|
||||
check_missing_fonts: true,
|
||||
font_source_url: String::new(),
|
||||
qnew_template: String::new(),
|
||||
font_source_input: String::new(),
|
||||
donation_prompt_version: String::new(),
|
||||
read_only: false,
|
||||
|
||||
@@ -251,6 +251,7 @@ const SNAP_ORDER: &[SnapType] = &[
|
||||
SnapType::Endpoint,
|
||||
SnapType::Midpoint,
|
||||
SnapType::Center,
|
||||
SnapType::GeometricCenter,
|
||||
SnapType::Node,
|
||||
SnapType::Quadrant,
|
||||
SnapType::Intersection,
|
||||
@@ -276,6 +277,7 @@ fn snap_bit(s: SnapType) -> Option<i32> {
|
||||
SnapType::Midpoint => 2,
|
||||
SnapType::Center => 4,
|
||||
SnapType::Node => 8,
|
||||
SnapType::GeometricCenter => 1024,
|
||||
SnapType::Quadrant => 16,
|
||||
SnapType::Intersection => 32,
|
||||
SnapType::Insertion => 64,
|
||||
@@ -430,6 +432,8 @@ pub struct UserSettings {
|
||||
pub cursor_type: CursorType,
|
||||
/// Explicit crosshair RGB. `None` keeps automatic background contrast.
|
||||
pub crosshair_color: Option<[u8; 3]>,
|
||||
/// Explicit object-snap marker RGB. `None` keeps the automatic colour.
|
||||
pub snap_marker_color: Option<[u8; 3]>,
|
||||
/// Model-space lineweight preview scale as a percentage.
|
||||
pub lineweight_display_scale: i32,
|
||||
/// Isometric drafting changes the grid and crosshair to the active axis
|
||||
@@ -459,6 +463,9 @@ pub struct UserSettings {
|
||||
/// so an intranet folder or a private GitHub raw folder both work.
|
||||
#[serde(default)]
|
||||
pub font_source_url: String,
|
||||
/// Drawing new tabs start from (QNEW template); empty = a blank drawing.
|
||||
#[serde(default)]
|
||||
pub qnew_template: String,
|
||||
/// App version whose donation prompt has been displayed.
|
||||
pub donation_prompt_version: String,
|
||||
/// The graphics verdict (`GpuStatus::identity()`) whose warning popup the
|
||||
@@ -819,6 +826,7 @@ impl Default for UserSettings {
|
||||
ncopy_bind: false,
|
||||
cursor_type: CursorType::Crosshair,
|
||||
crosshair_color: None,
|
||||
snap_marker_color: None,
|
||||
lineweight_display_scale: 100,
|
||||
isometric_drafting: false,
|
||||
iso_plane: IsoPlane::Left,
|
||||
@@ -827,6 +835,7 @@ impl Default for UserSettings {
|
||||
default_assoc_prompted: false,
|
||||
check_missing_fonts: true,
|
||||
font_source_url: String::new(),
|
||||
qnew_template: String::new(),
|
||||
donation_prompt_version: String::new(),
|
||||
gpu_warning_silenced: String::new(),
|
||||
disabled_plugins: Vec::new(),
|
||||
|
||||
@@ -1311,6 +1311,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.tabs[i]
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
self.sync_ribbon_layers();
|
||||
// Per-viewport layer visibility changes the resident
|
||||
// assembly, not any entity's tessellated geometry.
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
|
||||
+13
-2
@@ -681,6 +681,7 @@ impl OpenCADStudio {
|
||||
ncopy_bind: self.ncopy_bind,
|
||||
cursor_type: self.cursor_type,
|
||||
crosshair_color: self.crosshair_color,
|
||||
snap_marker_color: self.snap_marker_color,
|
||||
lineweight_display_scale: self.lineweight_display_scale,
|
||||
isometric_drafting: self.isometric_drafting,
|
||||
iso_plane: self.iso_plane,
|
||||
@@ -689,6 +690,7 @@ impl OpenCADStudio {
|
||||
default_assoc_prompted: self.default_assoc_prompted,
|
||||
check_missing_fonts: self.check_missing_fonts,
|
||||
font_source_url: self.font_source_url.clone(),
|
||||
qnew_template: self.qnew_template.clone(),
|
||||
donation_prompt_version: self.donation_prompt_version.clone(),
|
||||
gpu_warning_silenced: self.gpu_warning_silenced.clone(),
|
||||
disabled_plugins: {
|
||||
@@ -791,6 +793,11 @@ impl OpenCADStudio {
|
||||
.crosshair_color
|
||||
.map(crate::app::config::rgb_to_hex)
|
||||
.unwrap_or_default();
|
||||
self.snap_marker_color = s.snap_marker_color;
|
||||
self.snap_marker_color_input = s
|
||||
.snap_marker_color
|
||||
.map(crate::app::config::rgb_to_hex)
|
||||
.unwrap_or_default();
|
||||
self.lineweight_display_scale = s.lineweight_display_scale.clamp(25, 200);
|
||||
self.isometric_drafting = s.isometric_drafting;
|
||||
self.iso_plane = s.iso_plane;
|
||||
@@ -808,6 +815,7 @@ impl OpenCADStudio {
|
||||
self.default_assoc_prompted = s.default_assoc_prompted;
|
||||
self.check_missing_fonts = s.check_missing_fonts;
|
||||
self.font_source_url = s.font_source_url.clone();
|
||||
self.qnew_template = s.qnew_template.clone();
|
||||
self.font_source_input = s.font_source_url.clone();
|
||||
self.donation_prompt_version = s.donation_prompt_version.clone();
|
||||
self.gpu_warning_silenced = s.gpu_warning_silenced.clone();
|
||||
@@ -3603,10 +3611,13 @@ impl OpenCADStudio {
|
||||
pub(super) fn apply_plot_page_settings(&mut self) {
|
||||
let i = self.active_tab;
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
let ps = self.plot_settings_from_dialog(self.dialog_base_settings());
|
||||
if layout_name == "Model" {
|
||||
// Model keeps its page setup too (#1620); it has no sheet limits.
|
||||
self.tabs[i].scene.set_layout_plot_settings(&layout_name, &ps);
|
||||
self.tabs[i].dirty = true;
|
||||
return;
|
||||
}
|
||||
let ps = self.plot_settings_from_dialog(self.dialog_base_settings());
|
||||
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
|
||||
let plot_area = self.plot_dialog.area.clone();
|
||||
let center = ps.flags.plot_centered;
|
||||
@@ -5360,7 +5371,7 @@ impl OpenCADStudio {
|
||||
// into it when "Save changes to layout" is on, so the page setup
|
||||
// remembers the last plot the way other applications expect. The
|
||||
// runtime paper/scale choices drive this one plot operation either way.
|
||||
if !preview && d.save_to_layout && d.paper_space {
|
||||
if !preview && d.save_to_layout {
|
||||
self.apply_dialog_to_layout();
|
||||
} else {
|
||||
self.sync_dialog_plot_runtime();
|
||||
|
||||
@@ -1877,6 +1877,20 @@ impl OpenCADStudio {
|
||||
self.sync_ribbon_from_selection();
|
||||
// A fresh drawing starts with grid/snap off (its tile defaults).
|
||||
self.adopt_view_display(self.active_tab);
|
||||
// QNEW: start from the default template when one is set (#1113).
|
||||
let template = self.qnew_template.trim().to_string();
|
||||
if !template.is_empty() {
|
||||
match self.apply_template(&template) {
|
||||
Ok(_) => {
|
||||
self.tabs[idx].dirty = false;
|
||||
self.refresh_layer_panel();
|
||||
self.sync_ribbon_styles();
|
||||
}
|
||||
Err(error) => self.command_line.push_error(
|
||||
error["error"].as_str().unwrap_or("template"),
|
||||
),
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
@@ -5809,6 +5823,10 @@ impl OpenCADStudio {
|
||||
if id == crate::modules::insert::insert_block::GALLERY_ID {
|
||||
self.refresh_block_palette();
|
||||
}
|
||||
// The per-viewport freeze column follows the active viewport.
|
||||
if id == crate::ui::ribbon::widgets::LAYER_COMBO_ID {
|
||||
self.refresh_layer_panel();
|
||||
}
|
||||
self.ribbon.toggle_dropdown(&id);
|
||||
}
|
||||
Task::none()
|
||||
@@ -8005,6 +8023,39 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::QnewTemplateChanged(path) => {
|
||||
self.qnew_template = path;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
Message::QnewTemplateBrowse => Task::perform(
|
||||
async {
|
||||
crate::sys::file_dialog()
|
||||
.add_filter(
|
||||
crate::t!("Drawing Template (*.dwt)").as_ref(),
|
||||
&["dwt", "DWT", "dwg", "DWG", "dxf", "DXF"],
|
||||
)
|
||||
.pick_file()
|
||||
.await
|
||||
.map(|handle| crate::sys::handle_path(&handle).display().to_string())
|
||||
},
|
||||
|path| match path {
|
||||
Some(path) => Message::QnewTemplateChanged(path),
|
||||
None => Message::Noop,
|
||||
},
|
||||
),
|
||||
Message::SnapMarkerColorChanged(value) => {
|
||||
self.snap_marker_color_input = value.clone();
|
||||
if value.trim().is_empty() {
|
||||
self.snap_marker_color = None;
|
||||
self.persist_settings_if_changed();
|
||||
} else if let Some(rgb) = crate::app::config::parse_hex(&value) {
|
||||
self.snap_marker_color = Some(rgb);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::DefaultSaveFormatChanged(format) => {
|
||||
self.default_save_format = crate::io::canonical_save_format(&format).to_string();
|
||||
self.persist_settings_if_changed();
|
||||
@@ -9835,6 +9886,22 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── Plot Style Table ──────────────────────────────────────────────
|
||||
Message::LinetypeLoaded(Some(source)) => {
|
||||
let i = self.active_tab;
|
||||
self.push_undo_snapshot(i, "LINETYPE");
|
||||
let added = crate::io::linetypes::populate_document_from_source(
|
||||
&mut self.tabs[i].scene.document,
|
||||
&source,
|
||||
);
|
||||
if added > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
}
|
||||
self.command_line
|
||||
.push_output(crate::tf!("{added} linetype(s) loaded.").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
Message::LinetypeLoaded(None) => Task::none(),
|
||||
Message::PlotStyleLoad => {
|
||||
Task::perform(crate::io::pick_plot_style(), Message::PlotStyleLoaded)
|
||||
}
|
||||
|
||||
@@ -925,6 +925,13 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
iso_plane: self.iso_plane,
|
||||
snap_angle_deg: self.snap_angle_deg,
|
||||
pick_pending: self.pending_pick_label().is_some(),
|
||||
// An infinite-line preview runs along the arms; drop them
|
||||
// so the preview stays visible (#1437).
|
||||
hide_arms: tab
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|c| matches!(c.name(), "XLINE" | "RAY")),
|
||||
snap_color: self.snap_marker_color,
|
||||
},
|
||||
crate::ui::overlay::SelectionVisualOptions {
|
||||
area: self.model_space.selection_area,
|
||||
|
||||
@@ -383,11 +383,14 @@ impl OpenCADStudio {
|
||||
crate::ui::window::options::Folders::default()
|
||||
}
|
||||
},
|
||||
&self.qnew_template,
|
||||
self.double_click_block_refedit,
|
||||
self.double_click_block_attedit,
|
||||
self.cursor_type,
|
||||
self.crosshair_color,
|
||||
&self.crosshair_color_input,
|
||||
self.snap_marker_color,
|
||||
&self.snap_marker_color_input,
|
||||
self.lineweight_display_scale,
|
||||
&self.model_space,
|
||||
&self.model_bg_input,
|
||||
|
||||
Reference in new issue
Block a user