forked from 1b-agent/opencadstudio-upstream
fix(units): make the units selector reach what it names
The status-bar units button wrote INSUNITS, which nothing on screen reads. INSUNITS only tells the drawing how to scale content arriving from elsewhere, so picking a unit relabelled the drawing and changed nothing visible — the selector looked broken because the variable it moved was invisible. Point the button at LUNITS instead, the setting that decides how every coordinate and distance is written, and give the three questions the word "units" runs together a place each: - The button offers the five length formats, each shown beside a sample of itself, so Engineering and Architectural are told apart by sight. - UNITS / DDUNITS opens a real dialog: length and angle format and precision, the direction angle zero points, whether positive angles run clockwise, and the insertion unit. Its sample comes from the same formatters the drawing uses, so it cannot drift from the readout. - DWGUNITS converts. Model geometry, viewport framing, the header's length defaults and each dimension style's scale all move together in one undo step; paper space is left alone, since the sheet is still the same sheet. Locked layers are converted too — a half-converted drawing would be at two scales at once. The coordinate readout formatted at four decimal places whatever the drawing asked for; it now goes through the same format_length the properties panel uses. That made the angle formats reachable in turn, so degrees-minutes-seconds now carries the d/'/" marks that say which convention it is, and surveyor's units are written at all. Typed input is still plain decimal: a length the readout writes as 5'-6 1/2" cannot yet be typed back. ANGBASE and ANGDIR are stored but not yet applied to displayed directions. Closes #668 Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
cfc7c06c6c
commit
523b85f42b
14 files changed
+937
-69
No files matched your search
@@ -233,9 +233,11 @@ impl OpenCADStudio {
|
||||
"DSETTINGS" | "OSNAP" => {
|
||||
return Some(Task::done(Message::ToggleSnapPopup));
|
||||
}
|
||||
// UNITS — open the drawing-units picker (linear / angular format).
|
||||
// UNITS — length and angle formats, plus the insertion unit. The
|
||||
// status-bar button covers the length format alone; everything else
|
||||
// about how this drawing writes numbers is here.
|
||||
"UNITS" | "DDUNITS" => {
|
||||
return Some(Task::done(Message::ToggleUnitsPopup));
|
||||
return Some(Task::done(Message::OpenDrawingUnits));
|
||||
}
|
||||
|
||||
// ── CLEANSCREEN — collapse the surrounding panels for a full canvas ──
|
||||
|
||||
@@ -78,6 +78,41 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// DWGUNITS — change what one drawing unit measures, and convert the
|
||||
// model to match. The units pill only relabels, which is all the
|
||||
// label is for; this is the one place geometry moves. (#668)
|
||||
"DWGUNITS" => {
|
||||
use crate::command::KeywordCommand;
|
||||
use crate::modules::draw::units;
|
||||
// Buttons read as full names; the token behind each is the
|
||||
// abbreviation, since that is what gets typed. Unitless is left
|
||||
// off — there is nothing to convert to.
|
||||
let choices: Vec<(&str, &str, Option<&str>)> = units::all()
|
||||
.filter(|&(code, _)| code != 0)
|
||||
.map(|(code, label)| (label, units::short(code), None))
|
||||
.collect();
|
||||
// Built once: the table it comes from never changes, and
|
||||
// `KeywordCommand` wants a `&'static str`.
|
||||
static PROMPT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
let prompt = PROMPT.get_or_init(|| {
|
||||
let listed = units::all()
|
||||
.filter(|&(code, _)| code != 0)
|
||||
.map(|(code, label)| format!("{label}({})", units::short(code)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" / ");
|
||||
format!("DWGUNITS convert to [{listed}]:")
|
||||
});
|
||||
let current = units::label(self.tabs[i].scene.document.header.insertion_units);
|
||||
self.command_line
|
||||
.push_info(crate::tf!("DWGUNITS current unit: {current}").as_ref());
|
||||
let c = KeywordCommand::new("DWGUNITS", prompt.as_str(), choices);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
}
|
||||
cmd if cmd.starts_with("DWGUNITS ") => {
|
||||
return Some(self.convert_drawing_units(cmd.trim_start_matches("DWGUNITS").trim()));
|
||||
}
|
||||
|
||||
// LAYTRANS — translate this drawing's layers onto a set taken from
|
||||
// another drawing. Bare opens the mapping dialog; with a file it
|
||||
// maps every name the two drawings share and translates at once,
|
||||
@@ -716,6 +751,153 @@ impl OpenCADStudio {
|
||||
self.finish_layer_translation(i, report)
|
||||
}
|
||||
|
||||
/// Relabel the drawing's unit and convert the model to suit.
|
||||
///
|
||||
/// The label always changes — that is what was asked for. The geometry
|
||||
/// changes with it whenever both units measure something, which is what
|
||||
/// makes this different from picking a unit off the status bar. Unitless
|
||||
/// measures nothing, so a drawing coming from or going to it is relabelled
|
||||
/// and left alone.
|
||||
pub(in crate::app) fn convert_drawing_units(&mut self, name: &str) -> Task<Message> {
|
||||
use crate::command::EntityTransform;
|
||||
use crate::modules::draw::units;
|
||||
|
||||
let i = self.active_tab;
|
||||
let Some(to) = units::code_for_keyword(name) else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DWGUNITS: not a unit this drawing can use.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
let from = self.tabs[i].scene.document.header.insertion_units;
|
||||
if from == to {
|
||||
let label = units::label(to);
|
||||
self.command_line
|
||||
.push_output(crate::tf!("DWGUNITS: already in {label}.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
self.push_undo_snapshot(i, "DWGUNITS");
|
||||
self.tabs[i].scene.document.header.insertion_units = to;
|
||||
|
||||
let factor = units::conversion_factor(from, to).filter(|f| (f - 1.0).abs() > 1e-12);
|
||||
let (from_label, to_label) = (units::label(from), units::label(to));
|
||||
let Some(factor) = factor else {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"DWGUNITS: now {to_label}. Nothing was converted — {from_label} has no size to convert from."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let handles = units::model_space_handles(&self.tabs[i].scene);
|
||||
let moved = handles.len();
|
||||
// A locked layer is protected from editing, not from the drawing
|
||||
// changing what its numbers mean. Converting all but the locked layers
|
||||
// would leave the drawing at two scales at once, so the locks are
|
||||
// lifted for the duration and put back.
|
||||
let relocked: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|layer| layer.is_locked())
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
for name in &relocked {
|
||||
if let Some(layer) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
layer.flags.locked = false;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.transform_entities(
|
||||
&handles,
|
||||
&EntityTransform::Scale {
|
||||
center: glam::DVec3::ZERO,
|
||||
factor,
|
||||
},
|
||||
);
|
||||
for name in &relocked {
|
||||
if let Some(layer) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
layer.flags.locked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// A viewport frames the model, so its framing is measured in model
|
||||
// units and has to follow them. Left alone, every layout would suddenly
|
||||
// look at a region a thousand times too large. The paper side of the
|
||||
// ratio is untouched — the sheet is still the same sheet — so the ratio
|
||||
// itself moves the other way.
|
||||
//
|
||||
// `transform_viewport` covers the rectangle and the target but not the
|
||||
// framing, so the framing is done here for every viewport, and the
|
||||
// target only for the ones the scale above did not already reach.
|
||||
let scaled: std::collections::HashSet<acadrust::Handle> = handles.iter().copied().collect();
|
||||
let mut reframed = 0usize;
|
||||
for entity in self.tabs[i].scene.document.entities_mut() {
|
||||
let handle = entity.common().handle;
|
||||
let acadrust::entities::EntityType::Viewport(vp) = entity else {
|
||||
continue;
|
||||
};
|
||||
vp.view_center = vp.view_center * factor;
|
||||
vp.view_height *= factor;
|
||||
if vp.custom_scale.abs() > 1e-12 {
|
||||
vp.custom_scale /= factor;
|
||||
}
|
||||
if !scaled.contains(&handle) {
|
||||
vp.view_target = vp.view_target * factor;
|
||||
}
|
||||
reframed += 1;
|
||||
}
|
||||
// Sizes the drawing keeps as settings rather than as geometry: dash
|
||||
// lengths, default heights and widths, the radii the fillet and chamfer
|
||||
// commands start from. They are all lengths in the unit that just
|
||||
// changed, so they change with it or the next line drawn comes out at
|
||||
// the old scale. Angles and screen-relative sizes are left alone —
|
||||
// PDSIZE is a percentage of the screen when negative.
|
||||
let header = &mut self.tabs[i].scene.document.header;
|
||||
for length in [
|
||||
&mut header.linetype_scale,
|
||||
&mut header.current_entity_linetype_scale,
|
||||
&mut header.text_height,
|
||||
&mut header.trace_width,
|
||||
&mut header.sketch_increment,
|
||||
&mut header.thickness,
|
||||
&mut header.polyline_width,
|
||||
&mut header.fillet_radius,
|
||||
&mut header.chamfer_distance_a,
|
||||
&mut header.chamfer_distance_b,
|
||||
&mut header.chamfer_length,
|
||||
] {
|
||||
*length *= factor;
|
||||
}
|
||||
if header.point_display_size > 0.0 {
|
||||
header.point_display_size *= factor;
|
||||
}
|
||||
|
||||
// A dimension style holds its text height and arrow size in drawing
|
||||
// units and multiplies them by DIMSCALE, so moving that one factor
|
||||
// rescales the whole style. Zero means "take it from the viewport",
|
||||
// which is not a length and stays as it is.
|
||||
for style in self.tabs[i].scene.document.dim_styles.iter_mut() {
|
||||
if style.dimscale.abs() > 1e-12 {
|
||||
style.dimscale *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"DWGUNITS: {from_label} to {to_label} — {moved} object(s) scaled by {factor}, {reframed} viewport(s) reframed."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Open the translator, optionally with a standard already loaded.
|
||||
pub(in crate::app) fn open_layer_translator(
|
||||
&mut self,
|
||||
|
||||
@@ -298,6 +298,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
// Layer management.
|
||||
"LAYDEL",
|
||||
"LAYTRANS",
|
||||
"DWGUNITS",
|
||||
"LAYMRG",
|
||||
"LAYERSTATE",
|
||||
"LAS",
|
||||
|
||||
@@ -408,6 +408,8 @@ pub(super) struct OpenCADStudio {
|
||||
last_layer_translation: Option<crate::modules::draw::layers::laytrans::Report>,
|
||||
/// The layer-translator dialog's working state while it is open.
|
||||
layer_translator: Option<crate::ui::window::layer_translator::State>,
|
||||
/// Working copy of the Drawing Units dialog; `None` while it is closed.
|
||||
drawing_units: Option<crate::ui::window::drawing_units::State>,
|
||||
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
|
||||
/// press-drag draws a rectangle marquee.
|
||||
pick_drag_rect: bool,
|
||||
@@ -1495,6 +1497,7 @@ pub enum ModalKind {
|
||||
Layers,
|
||||
LayerStateManager,
|
||||
LayerTranslator,
|
||||
DrawingUnits,
|
||||
LayerStateEditor,
|
||||
Plot,
|
||||
PrintAll,
|
||||
@@ -2196,6 +2199,14 @@ pub enum Message {
|
||||
CloseUnitsPopup,
|
||||
/// Set the drawing units (INSUNITS) for the active drawing.
|
||||
SetDrawingUnits(i16),
|
||||
/// LUNITS — how lengths are written, from the status-bar units button.
|
||||
SetLinearFormat(i16),
|
||||
/// Open the Drawing Units dialog, seeded from the active drawing.
|
||||
OpenDrawingUnits,
|
||||
/// One field of the Drawing Units dialog changed.
|
||||
DrawingUnitsField(crate::ui::window::drawing_units::Field),
|
||||
/// Drawing Units OK — write the working copy into the drawing.
|
||||
DrawingUnitsApply,
|
||||
/// Toggle the Isolate pill's action menu open/closed.
|
||||
ToggleIsolatePopup,
|
||||
/// Close the Isolate action menu.
|
||||
@@ -3009,6 +3020,7 @@ impl OpenCADStudio {
|
||||
select_remove_mode: false,
|
||||
last_layer_translation: None,
|
||||
layer_translator: None,
|
||||
drawing_units: None,
|
||||
pick_drag_rect: false,
|
||||
perf_hud: false,
|
||||
cycle_candidates: None,
|
||||
|
||||
@@ -3534,11 +3534,107 @@ impl OpenCADStudio {
|
||||
self.units_popup_open = false;
|
||||
Task::none()
|
||||
}
|
||||
Message::OpenDrawingUnits => {
|
||||
self.units_popup_open = false;
|
||||
let header = &self.tabs[self.active_tab].scene.document.header;
|
||||
self.drawing_units = Some(crate::ui::window::drawing_units::State {
|
||||
linear_format: header.linear_unit_format,
|
||||
linear_precision: header.linear_unit_precision,
|
||||
angular_format: header.angular_unit_format,
|
||||
angular_precision: header.angular_unit_precision,
|
||||
clockwise: header.angle_direction != 0,
|
||||
base_angle: format!("{:.6}", header.angle_base.to_degrees())
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
.to_string(),
|
||||
insertion_units: header.insertion_units,
|
||||
});
|
||||
self.active_modal = Some(crate::app::ModalKind::DrawingUnits);
|
||||
Task::none()
|
||||
}
|
||||
Message::DrawingUnitsField(field) => {
|
||||
use crate::ui::window::drawing_units::Field;
|
||||
let Some(state) = self.drawing_units.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
match field {
|
||||
Field::LinearFormat(v) => state.linear_format = v,
|
||||
Field::LinearPrecision(v) => state.linear_precision = v,
|
||||
Field::AngularFormat(v) => state.angular_format = v,
|
||||
Field::AngularPrecision(v) => state.angular_precision = v,
|
||||
Field::Clockwise(v) => state.clockwise = v,
|
||||
// Kept as typed so a lone "-" or a trailing "." survives
|
||||
// until the number it is becoming is finished.
|
||||
Field::BaseAngle(v) => state.base_angle = v,
|
||||
Field::InsertionUnits(v) => state.insertion_units = v,
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DrawingUnitsApply => {
|
||||
let Some(state) = self.drawing_units.take() else {
|
||||
self.active_modal = None;
|
||||
return Task::none();
|
||||
};
|
||||
self.active_modal = None;
|
||||
let i = self.active_tab;
|
||||
self.push_undo_snapshot(i, "UNITS");
|
||||
let header = &mut self.tabs[i].scene.document.header;
|
||||
header.linear_unit_format = state.linear_format;
|
||||
header.linear_unit_precision = state.linear_precision;
|
||||
header.angular_unit_format = state.angular_format;
|
||||
header.angular_unit_precision = state.angular_precision;
|
||||
header.angle_direction = i16::from(state.clockwise);
|
||||
// A base angle that will not parse is a half-finished edit, not
|
||||
// an instruction to move zero — leave the drawing's own value.
|
||||
if let Ok(degrees) = state.base_angle.trim().parse::<f64>() {
|
||||
header.angle_base = degrees.to_radians();
|
||||
}
|
||||
header.insertion_units = state.insertion_units;
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
Message::SetLinearFormat(code) => {
|
||||
self.units_popup_open = false;
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.document.header.linear_unit_format == code {
|
||||
return Task::none();
|
||||
}
|
||||
// Every displayed length is written through this, so the whole
|
||||
// drawing re-reads at once — no geometry moves, only the way it
|
||||
// is written down.
|
||||
self.push_undo_snapshot(i, "LUNITS");
|
||||
self.tabs[i].scene.document.header.linear_unit_format = code;
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.refresh_properties();
|
||||
let label = crate::modules::draw::units::linear_format_label(code);
|
||||
self.command_line
|
||||
.push_output(crate::tf!("Length format is now {label}.").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
Message::SetDrawingUnits(code) => {
|
||||
self.units_popup_open = false;
|
||||
let i = self.active_tab;
|
||||
let current = self.tabs[i].scene.document.header.insertion_units;
|
||||
if current == code {
|
||||
return Task::none();
|
||||
}
|
||||
// Relabelling only: the geometry keeps every number it had, and
|
||||
// now says they count something else. Say so, because picking a
|
||||
// unit and seeing the drawing sit still otherwise reads as the
|
||||
// menu having done nothing. (#668)
|
||||
self.push_undo_snapshot(i, "UNITS");
|
||||
self.tabs[i].scene.document.header.insertion_units = code;
|
||||
self.tabs[i].dirty = true;
|
||||
let label = crate::modules::draw::units::label(code);
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"Drawing unit is now {label}. Geometry unchanged — use DWGUNITS to convert it."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
Message::ToggleIsolatePopup => {
|
||||
|
||||
+14
-1
@@ -1658,6 +1658,19 @@ impl OpenCADStudio {
|
||||
let active_block = tab
|
||||
.active_block_edit_session()
|
||||
.map(|session| session.block_name.clone());
|
||||
// The coordinate readout formats through the same helper
|
||||
// the properties panel uses, so it has to see the drawing's
|
||||
// linear-unit settings. Properties happens to set this on
|
||||
// every refresh; the status bar redraws on its own schedule
|
||||
// and cannot rely on that having happened.
|
||||
crate::entities::common::set_unit_context(
|
||||
crate::entities::common::UnitContext {
|
||||
lunits: tab.scene.document.header.linear_unit_format,
|
||||
luprec: tab.scene.document.header.linear_unit_precision,
|
||||
aunits: tab.scene.document.header.angular_unit_format,
|
||||
auprec: tab.scene.document.header.angular_unit_precision,
|
||||
},
|
||||
);
|
||||
let status_menu_data = crate::ui::statusbar::StatusMenuData {
|
||||
layout_names: layout_names.clone(),
|
||||
polar_custom_input: &self.polar_custom_input,
|
||||
@@ -1703,7 +1716,7 @@ impl OpenCADStudio {
|
||||
last_coord,
|
||||
picking,
|
||||
self.clean_screen,
|
||||
tab.scene.document.header.insertion_units,
|
||||
tab.scene.document.header.linear_unit_format,
|
||||
tab.scene.is_isolation_active(),
|
||||
tab.scene.transparency_display,
|
||||
self.quick_properties,
|
||||
|
||||
@@ -20,6 +20,7 @@ impl OpenCADStudio {
|
||||
Some(K::Layers) => crate::tr!("modal-layer-manager"),
|
||||
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
|
||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||
Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"),
|
||||
Some(K::Plot) => crate::tr!("modal-plot"),
|
||||
Some(K::PrintAll) => t!("Print All").into_owned(),
|
||||
@@ -185,6 +186,12 @@ impl OpenCADStudio {
|
||||
)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::DrawingUnits => {
|
||||
let state = self.drawing_units.as_ref()?;
|
||||
sized_flow(ex, 560, 420, |flow| {
|
||||
crate::ui::window::drawing_units::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::LayerStateManager => {
|
||||
let states = self.tabs[self.active_tab].scene.document.layer_states();
|
||||
sized_flow(
|
||||
|
||||
Reference in new issue
Block a user