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
@@ -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));
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user