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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-06 15:59:30 +03:00
1 parent abf302eba5
commit 178c7dff41
70 files changed
+1227 -145

No files matched your search

+90 -16
View File
@@ -9,7 +9,7 @@
// Pick two lines (line-only; arcs are not chamferable).
// Finds intersection, backs off dist1 along line 1 and dist2 along line 2.
use codec::entities::{Arc as ArcEnt, Line as LineEnt, LwPolyline};
use codec::entities::{Arc as ArcEnt, Line as LineEnt, LwPolyline, LwVertex};
// Shared plane geometry, from opencadkernel via the local adapters.
use super::geom;
@@ -1354,6 +1354,9 @@ enum FilletStep {
// Radius may be typed directly or measured by picking two points.
WaitingForRadius,
/// Trim / No trim: whether the picked objects are cut back to the arc.
TrimMode,
RadiusSecondPoint {
first: DVec3,
},
@@ -1398,12 +1401,16 @@ impl FilletCommand {
/// Switch to the radius sub-step, remembering the first pick (if any) so
/// it can be restored afterwards.
fn enter_radius_substep(&mut self) {
self.enter_substep(FilletStep::WaitingForRadius);
}
fn enter_substep(&mut self, step: FilletStep) {
self.resume_second = if let FilletStep::Second { h1, e1, click1 } = &self.step {
Some((*h1, e1.clone(), *click1))
} else {
None
};
self.step = FilletStep::WaitingForRadius;
self.step = step;
}
/// Leave the radius sub-step, resuming the second pick when a first object
@@ -1415,7 +1422,12 @@ impl FilletCommand {
};
}
fn continue_after_fillet(&mut self, replacements: Vec<(Handle, Vec<EntityType>)>) -> CmdResult {
fn continue_after_fillet(&mut self, mut replacements: Vec<(Handle, Vec<EntityType>)>) -> CmdResult {
for entity in replacements.iter_mut().flat_map(|(_, entities)| entities.iter_mut()) {
if let EntityType::LwPolyline(poly) = entity {
drop_zero_length_segments(poly);
}
}
self.all_entities.retain(|entity| {
!replacements
.iter()
@@ -1488,6 +1500,12 @@ impl CadCommand for FilletCommand {
FilletStep::RadiusSecondPoint { .. } => {
crate::t!("FILLET Specify second point for radius:").into_owned()
}
FilletStep::TrimMode => if defaults::get_fillet_trim() {
crate::t!("FILLET Enter Trim mode option [Trim/No trim] <Trim>:")
} else {
crate::t!("FILLET Enter Trim mode option [Trim/No trim] <No trim>:")
}
.into_owned(),
FilletStep::Second { .. } => {
crate::tf!(
"FILLET Select second object (Line/Arc/LwPolyline) [R={:.4}]:",
@@ -1509,10 +1527,14 @@ impl CadCommand for FilletCommand {
}
opts.push(CmdOption::new("Polyline", "P"));
opts.push(CmdOption::new("Radius", "R"));
opts.push(CmdOption::new("Trim", "T"));
opts
}
FilletStep::Second { .. } => {
vec![CmdOption::new("Radius", "R")]
vec![CmdOption::new("Radius", "R"), CmdOption::new("Trim", "T")]
}
FilletStep::TrimMode => {
vec![CmdOption::new("Trim", "T"), CmdOption::new("No trim", "N")]
}
FilletStep::Polyline
| FilletStep::WaitingForRadius
@@ -1521,7 +1543,7 @@ impl CadCommand for FilletCommand {
}
fn wants_text_input(&self) -> bool {
matches!(self.step, FilletStep::WaitingForRadius)
matches!(self.step, FilletStep::WaitingForRadius | FilletStep::TrimMode)
}
fn dyn_field(&self) -> crate::command::DynField {
@@ -1552,6 +1574,16 @@ impl CadCommand for FilletCommand {
// Invalid — stay and re-prompt
Some(CmdResult::NeedPoint)
}
FilletStep::TrimMode => {
match text.trim().to_uppercase().as_str() {
"" => {}
"T" | "TRIM" => defaults::set_fillet_trim(true),
"N" | "NO TRIM" | "NOTRIM" => defaults::set_fillet_trim(false),
_ => return Some(CmdResult::NeedPoint),
}
self.resume_after_radius();
Some(CmdResult::NeedPoint)
}
FilletStep::First | FilletStep::Second { .. } => {
let t = text.trim();
let upper = t.to_uppercase();
@@ -1562,6 +1594,10 @@ impl CadCommand for FilletCommand {
if upper == "U" || upper == "UNDO" {
return self.undo_last();
}
if upper == "T" || upper == "TRIM" {
self.enter_substep(FilletStep::TrimMode);
return Some(CmdResult::NeedPoint);
}
// "R" alone → enter sub-step to collect radius
if upper == "R" {
self.enter_radius_substep();
@@ -1594,6 +1630,7 @@ impl CadCommand for FilletCommand {
self.step,
FilletStep::WaitingForRadius
| FilletStep::RadiusSecondPoint { .. }
| FilletStep::TrimMode
)
}
@@ -1619,7 +1656,8 @@ impl CadCommand for FilletCommand {
match &self.step {
FilletStep::WaitingForRadius
| FilletStep::RadiusSecondPoint { .. } => {
| FilletStep::RadiusSecondPoint { .. }
| FilletStep::TrimMode => {
return CmdResult::NeedPoint;
}
@@ -1689,6 +1727,17 @@ impl CadCommand for FilletCommand {
if let Some(e2) = e2 {
match compute_fillet_entities(&e1, click1, &e2, click, self.radius) {
// No trim: the objects stay as they are; only the arc
// is added (#1558).
Some((_, _, maybe_arc)) if !defaults::get_fillet_trim() => {
let Some(arc) = maybe_arc else {
return CmdResult::NeedPoint;
};
self.step = FilletStep::First;
self.resume_second = None;
self.made += 1;
CmdResult::CommitEntity(arc)
}
Some((new_e1, new_e2, maybe_arc)) => {
let mut first_replacements = vec![new_e1];
if let Some(arc) = maybe_arc {
@@ -1719,7 +1768,8 @@ impl CadCommand for FilletCommand {
match &self.step {
FilletStep::WaitingForRadius
| FilletStep::RadiusSecondPoint { .. } => vec![],
| FilletStep::RadiusSecondPoint { .. }
| FilletStep::TrimMode => vec![],
FilletStep::Polyline => {
let preview = self
.entity_index
@@ -1780,20 +1830,21 @@ impl CadCommand for FilletCommand {
if let Some((new_e1, new_e2, maybe_arc)) =
compute_fillet_entities(&e1, click1, &e2, click, self.radius)
{
let mut out = vec![
WireModel::solid(
let mut out = Vec::new();
if defaults::get_fillet_trim() {
out.push(WireModel::solid(
"fillet_e1".into(),
entity_pts(&new_e1),
WireModel::CYAN,
false,
),
WireModel::solid(
));
out.push(WireModel::solid(
"fillet_e2".into(),
entity_pts(&new_e2),
WireModel::CYAN,
false,
),
];
));
}
if let Some(arc) = maybe_arc {
out.push(WireModel::solid(
"fillet_arc".into(),
@@ -1856,6 +1907,29 @@ impl CadCommand for FilletCommand {
}
}
/// A radius-0 fillet over an existing arc leaves its two ends on one point;
/// the zero-length segment between them blocked every later fillet of that
/// corner (#1422). The later vertex carries the next segment, so it survives.
fn drop_zero_length_segments(poly: &mut LwPolyline) {
let same = |a: &LwVertex, b: &LwVertex| {
(a.location.x - b.location.x).abs() < 1e-9 && (a.location.y - b.location.y).abs() < 1e-9
};
let mut i = 0;
while i + 1 < poly.vertices.len() && poly.vertices.len() > 2 {
if same(&poly.vertices[i], &poly.vertices[i + 1]) {
poly.vertices.remove(i);
} else {
i += 1;
}
}
if poly.is_closed && poly.vertices.len() > 2 {
let n = poly.vertices.len();
if same(&poly.vertices[n - 1], &poly.vertices[0]) {
poly.vertices.pop();
}
}
}
// ══════════════════════════════════════════════════════════════════════════
// ChamferCommand
// ══════════════════════════════════════════════════════════════════════════
@@ -2448,16 +2522,16 @@ mod tests {
1.0,
vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)],
);
assert_eq!(keywords(&command), ["P", "R"]);
assert_eq!(keywords(&command), ["P", "R", "T"]);
assert!(command.on_text_input("U").is_none(), "nothing to undo yet");
command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0));
assert!(matches!(
command.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)),
CmdResult::ReplaceManyContinue(_)
));
assert_eq!(keywords(&command), ["U", "P", "R"]);
assert_eq!(keywords(&command), ["U", "P", "R", "T"]);
assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument)));
assert_eq!(keywords(&command), ["P", "R"]);
assert_eq!(keywords(&command), ["P", "R", "T"]);
// The host hands the restored document back; the cache follows it.
let mut doc = codec::CadDocument::new();
let _ = doc.add_entity(line(0.0, 0.0, 20.0, 0.0, 7));
+53
View File
@@ -9,6 +9,7 @@ use glam::DVec3;
use rustc_hash::FxHashMap as HashMap;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
use crate::t;
const TAU: f64 = std::f64::consts::TAU;
@@ -188,6 +189,24 @@ impl PeditCommand {
}
}
/// WCS location of a vertex of the polyline being edited.
fn vertex_world(&self, index: usize) -> Option<DVec3> {
let entity = self.target.and_then(|handle| self.entities.get(&handle.value()))?;
let (ocs, n) = match entity {
EntityType::LwPolyline(p) => {
let v = p.vertices.get(index)?;
((v.location.x, v.location.y, p.elevation), &p.normal)
}
EntityType::Polyline2D(p) => {
let v = p.vertices.get(index)?;
((v.location.x, v.location.y, p.elevation), &p.normal)
}
_ => return None,
};
let (x, y, z) = crate::scene::view::transform::ocs_point_to_wcs(ocs, (n.x, n.y, n.z));
Some(DVec3::new(x, y, z))
}
fn vertex_count(&self) -> usize {
self.target.and_then(|handle| self.entities.get(&handle.value())).map_or(0, |entity| match entity {
EntityType::LwPolyline(polyline) => polyline.vertices.len(),
@@ -237,6 +256,40 @@ impl CadCommand for PeditCommand {
"PEDIT"
}
/// Edit-vertex steps mark the current vertex with a screen-sized X (#1628).
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
let index = match self.mode {
Mode::PolyVertex(i)
| Mode::PolyTangent(i)
| Mode::PolyMove(i)
| Mode::PolyInsert(i)
| Mode::PolyWidthStart(i, _)
| Mode::PolyWidthEnd(i, _) => i,
Mode::PolyRange { end, .. } => end,
_ => return None,
};
let origin = self.vertex_world(index)?;
let at = |dx: f64, dy: f64| {
let p = origin + DVec3::new(dx, dy, 0.0);
[p.x, p.y, p.z]
};
let h = 0.5;
let mut wire = WireModel::solid_f64(
"pedit_vertex_marker".into(),
vec![at(-h, -h), at(h, h), [f64::NAN; 3], at(-h, h), at(h, -h)],
WireModel::CYAN,
false,
);
wire.point_marker = Some(crate::scene::model::wire_model::PointMarker {
origin,
normal: DVec3::Z,
axis_x: DVec3::X,
axis_y: DVec3::Y,
viewport_percent: 3.0,
});
Some(wire)
}
fn prompt(&self) -> String {
match &self.mode {
Mode::PickTarget => {