feat(dynamic-input): per-step specs for rectangle, polygon and ellipse

Extend the DynSpec coverage and add the guides those commands need:

- Rectangle: two-corner enters width/height as unsigned magnitudes (the
  sign follows the cursor side, like the angle entry), drawn with the
  rectangle as the guide; rotated rectangle's height is a single distance
  measured square to the base edge (dimension line offset off the edge
  with extension lines); centre rectangle shows half-width/half-height.
- Polygon: inscribed/circumscribed vertex step shows radius + rotation.
- Ellipse: minor-axis step (all three modes) measures the half-length
  perpendicular to the major axis and draws that semi-axis from the
  centre; angle steps anchor their arc at the centre.

Adds DynGuide::Perp / PerpDim (perpendicular measure to a reference line)
and DynSpec::ref_point, plus Width/Height role handling (unsigned display,
cursor-signed commit) and diameter value scaling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-15 18:46:34 +03:00
1 parent b0ee1c39bd
commit e31dbcb5c4
8 files changed
+296 -10

No files matched your search

+7
View File
@@ -132,6 +132,11 @@ pub(super) struct DocumentTab {
/// World-space anchor the current step's values are measured from. `None`
/// falls back to `App::last_point`.
pub(super) dyn_anchor: Option<glam::Vec3>,
/// Far end of a reference line through `dyn_anchor` (for the `Perp` guide:
/// the base edge / major axis the offset is measured square to).
pub(super) dyn_ref: Option<glam::Vec3>,
/// `dyn_ref` projected to viewport pixels.
pub(super) dyn_ref_screen: Option<iced::Point>,
/// Index of the field that TAB has focused (the one keystrokes edit).
pub(super) dyn_active: usize,
pub(super) history: HistoryState,
@@ -195,6 +200,8 @@ impl DocumentTab {
dyn_fields: Vec::new(),
dyn_guide: crate::command::DynGuide::Polar,
dyn_anchor: None,
dyn_ref: None,
dyn_ref_screen: None,
dyn_active: 0,
history: HistoryState::default(),
active_layer: "0".to_string(),
+46 -6
View File
@@ -2545,14 +2545,16 @@ impl OpenCADStudio {
// Project the step anchor (an explicit `dyn_anchor` or the
// last point) so the dynamic-input overlay can place its
// guide geometry and labels.
let anchor = self.tabs[i].dyn_anchor.or(self.last_point);
self.tabs[i].last_point_screen = anchor.map(|bp| {
let project = |bp: glam::Vec3| {
let ndc = view_proj.project_point3(bp);
iced::Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height,
)
});
};
let anchor = self.tabs[i].dyn_anchor.or(self.last_point);
self.tabs[i].last_point_screen = anchor.map(project);
self.tabs[i].dyn_ref_screen = self.tabs[i].dyn_ref.map(project);
// Entity-pick previews (TRIM/EXTEND/FILLET…) compare the
// cursor against WCS document entities and return WCS wires.
@@ -7590,6 +7592,7 @@ impl OpenCADStudio {
_ => crate::command::DynGuide::None,
};
self.tabs[i].dyn_anchor = self.last_point;
self.tabs[i].dyn_ref = None;
}
/// Apply an explicit per-step [`DynSpec`](crate::command::DynSpec): rebuild
@@ -7611,6 +7614,7 @@ impl OpenCADStudio {
crate::command::DynAnchor::LastPoint => self.last_point,
crate::command::DynAnchor::Point(p) => Some(p),
};
self.tabs[i].dyn_ref = spec.ref_point;
}
/// Track cursor dwell over a selected entity's grip. Sets
@@ -7783,15 +7787,29 @@ impl OpenCADStudio {
.or(self.last_point)
.unwrap_or(glam::Vec3::ZERO);
// Buffer value parsed as f32 (de-scaled by the role so a typed diameter
// becomes a radius), or the supplied geometric live value.
// becomes a radius), or the supplied geometric live value. Width/Height
// are shown unsigned, so a typed value takes the sign of the cursor's
// delta on that axis (`live` is the signed delta in the cartesian arms).
let val = |idx: usize, live: f32| -> f32 {
fields[idx]
match fields[idx]
.buffer
.as_ref()
.map(|s| s.trim().replace(',', "."))
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32))
.map(|v| v / fields[idx].role.value_scale())
.unwrap_or(live)
{
Some(v) => {
if matches!(
fields[idx].role,
crate::command::DynRole::Width | crate::command::DynRole::Height
) {
v.abs().copysign(live)
} else {
v
}
}
None => live,
}
};
let dx = w.x - base.x;
let dy = w.y - base.y;
@@ -7812,6 +7830,28 @@ impl OpenCADStudio {
}
};
let comps: Vec<DynComponent> = fields.iter().map(|f| f.component).collect();
// Perpendicular offset: a single distance measured square to the
// reference line (anchor → dyn_ref). The committed point lies on the
// perpendicular through the anchor at that offset; the command projects
// it. Untyped tracks the cursor's signed offset; typed takes the
// cursor's side.
if let (Some(ref_pt), [DynComponent::Distance]) =
(self.tabs[i].dyn_ref, comps.as_slice())
{
let axis = (ref_pt - base).normalize_or_zero();
let perp = glam::Vec3::new(-axis.y, axis.x, 0.0);
let signed = (w - base).dot(perp);
let typed = fields[0]
.buffer
.as_ref()
.map(|s| s.trim().replace(',', "."))
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32));
let h = match typed {
Some(v) => v.abs().copysign(signed),
None => signed,
};
return Some(base + perp * h);
}
// DYN-on defaults to RELATIVE coordinates when a base point is set
// (see #26 / #35). The live cartesian fallback is the cursor
// position relative to base; typed values are relative deltas.
+6 -2
View File
@@ -763,6 +763,7 @@ impl OpenCADStudio {
Some(overlay::dynamic_input_overlay(
tab.last_cursor_screen,
tab.last_point_screen,
tab.dyn_ref_screen,
tab.dyn_guide,
boxes,
prompt,
@@ -4084,9 +4085,12 @@ fn dyn_component_value(
// in `dyn_resolve_point` so the live preview and the committed
// coordinate use the same frame. See #35.
let has_base = base.is_some();
// Width / Height read as unsigned magnitudes (the sign is taken from the
// cursor side on commit), matching the rectangle's two-edge entry.
let wh = matches!(f.role, crate::command::DynRole::Width | crate::command::DynRole::Height);
match f.component {
DynComponent::X if has_base => format!("{:.4}", dx),
DynComponent::Y if has_base => format!("{:.4}", dy),
DynComponent::X if has_base => format!("{:.4}", if wh { dx.abs() } else { dx }),
DynComponent::Y if has_base => format!("{:.4}", if wh { dy.abs() } else { dy }),
DynComponent::Z if has_base => "0.0000".to_string(),
DynComponent::X => format!("{:.4}", w.x),
DynComponent::Y => format!("{:.4}", w.y),