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 <[email protected]>
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

+2
View File
@@ -201,6 +201,7 @@ impl CadCommand for CircleCommand {
anchor: DynAnchor::Point(c),
fields: vec![DynFieldSpec::new(DynRole::Radius)],
guide: DynGuide::Radius,
ref_point: None,
}),
}
}
@@ -288,6 +289,7 @@ impl CadCommand for CircleCDCommand {
anchor: DynAnchor::Point(c),
fields: vec![DynFieldSpec::new(DynRole::Diameter)],
guide: DynGuide::Radius,
ref_point: None,
}),
}
}
+81
View File
@@ -182,6 +182,31 @@ impl CadCommand for EllipseCommand {
_ => None,
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
match &self.step {
// Center + major axis endpoint: ordinary point picks (legacy polar
// anchored at the previous point).
CtrStep::Center | CtrStep::MajorAxis { .. } => None,
// Minor axis: half-length measured square to the major axis. Show
// the perpendicular drop from the cursor onto the major axis.
CtrStep::MinorRatio { center, major } => Some(DynSpec {
anchor: DynAnchor::Point(*center),
fields: vec![DynFieldSpec::new(DynRole::Distance)],
guide: DynGuide::Perp,
ref_point: Some(*center + *major),
}),
}
}
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> {
if let CtrStep::MinorRatio { center, major } = &self.step {
Some((minor_ratio(*center, *major, cursor) * major.length()) as f64)
} else {
None
}
}
}
// ── 2. Axis, End mode ─────────────────────────────────────────────────────
@@ -270,6 +295,30 @@ impl CadCommand for EllipseAxisCommand {
}
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
match &self.step {
// Endpoints define the full major axis — legacy polar (anchored at
// the previous point) is right.
AxisStep::Pt1 | AxisStep::Pt2 { .. } => None,
// Minor half-length, square to the major axis.
AxisStep::MinorRatio { center, major } => Some(DynSpec {
anchor: DynAnchor::Point(*center),
fields: vec![DynFieldSpec::new(DynRole::Distance)],
guide: DynGuide::Perp,
ref_point: Some(*center + *major),
}),
}
}
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> {
if let AxisStep::MinorRatio { center, major } = &self.step {
Some((minor_ratio(*center, *major, cursor) * major.length()) as f64)
} else {
None
}
}
}
// ── 3. Ellipse Arc mode ───────────────────────────────────────────────────
@@ -487,6 +536,38 @@ impl CadCommand for EllipseArcCommand {
_ => None,
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
match &self.step {
ArcStep::Center | ArcStep::MajorAxis { .. } => None,
// Minor half-length, square to the major axis.
ArcStep::MinorRatio { center, major } => Some(DynSpec {
anchor: DynAnchor::Point(*center),
fields: vec![DynFieldSpec::new(DynRole::Distance)],
guide: DynGuide::Perp,
ref_point: Some(*center + *major),
}),
// Start / end sweep angles measured at the centre (the last point
// is the previous pick, so anchor the angle arc at the centre).
ArcStep::StartAngle { center, .. } | ArcStep::EndAngle { center, .. } => {
Some(DynSpec {
anchor: DynAnchor::Point(*center),
fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::Polar,
ref_point: None,
})
}
}
}
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> {
if let ArcStep::MinorRatio { center, major } = &self.step {
Some((minor_ratio(*center, *major, cursor) * major.length()) as f64)
} else {
None
}
}
}
// ── Internal helpers ──────────────────────────────────────────────────────
+80
View File
@@ -168,6 +168,21 @@ impl CadCommand for RectCommand {
[a.x, pt.y, a.z],
]))
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Opposite corner: enter width and height (signed deltas from the first
// corner), with the rectangle drawn as the guide. First corner is a
// normal point pick.
self.a.map(|a| DynSpec {
anchor: DynAnchor::Point(a),
fields: vec![
DynFieldSpec::new(DynRole::Width),
DynFieldSpec::new(DynRole::Height),
],
guide: DynGuide::RectSides,
ref_point: None,
})
}
}
// ── Command: Rectangle — Rotated (RECT_ROT) ──────────────────────────────
@@ -256,6 +271,29 @@ impl CadCommand for RectRotCommand {
_ => None,
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Step 0: corner A (point). Step 1: adjacent corner — the base edge,
// needs direction + length (legacy polar). Step 2: height — measured
// square to the fixed base edge A→B, so show the perpendicular drop
// and take the perpendicular distance (no angle).
(self.step == 2).then(|| DynSpec {
anchor: DynAnchor::Point(self.b),
fields: vec![DynFieldSpec::new(DynRole::Distance)],
guide: DynGuide::PerpDim,
ref_point: Some(self.a),
})
}
fn dyn_live_value(&self, cursor: Vec3) -> Option<f64> {
// Live height = perpendicular distance from the cursor to the base edge.
(self.step == 2).then(|| {
let dir = (self.b - self.a).normalize_or_zero();
let perp = Vec3::new(-dir.y, dir.x, 0.0);
(cursor - self.b).dot(perp).abs() as f64
})
}
}
// ── Command: Rectangle — Center (RECT_CEN) ───────────────────────────────
@@ -320,6 +358,20 @@ impl CadCommand for RectCenCommand {
[c.x - hw, c.y + hh, c.z],
]))
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Corner from the centre gives the half-width / half-height; show them
// on dotted axis legs out of the centre.
self.center.map(|c| DynSpec {
anchor: DynAnchor::Point(c),
fields: vec![
DynFieldSpec::new(DynRole::Width),
DynFieldSpec::new(DynRole::Height),
],
guide: DynGuide::AxisDelta,
ref_point: None,
})
}
}
// ── Command: Polygon — Inscribed (POLY) ──────────────────────────────────
@@ -359,6 +411,20 @@ impl CadCommand for PolyCommand {
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Vertex on the circle: radius from the centre + rotation angle.
(self.step == 2).then(|| DynSpec {
anchor: DynAnchor::Point(self.center),
fields: vec![
DynFieldSpec::new(DynRole::Radius),
DynFieldSpec::new(DynRole::Angle),
],
guide: DynGuide::Polar,
ref_point: None,
})
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if let Ok(n) = text.trim().parse::<u32>() {
if (3..=1024).contains(&n) {
@@ -469,6 +535,20 @@ impl CadCommand for PolyCCommand {
Some(CmdResult::NeedPoint)
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Edge-midpoint distance (apothem) from the centre + rotation.
(self.step == 2).then(|| DynSpec {
anchor: DynAnchor::Point(self.center),
fields: vec![
DynFieldSpec::new(DynRole::Radius),
DynFieldSpec::new(DynRole::Angle),
],
guide: DynGuide::Polar,
ref_point: None,
})
}
fn prompt(&self) -> String {
match self.step {
0 => format!("POLYGON C Enter number of sides <{}>:", self.sides),