forked from 1b-agent/opencadstudio-upstream
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:
1 parent
b0ee1c39bd
commit
e31dbcb5c4
8 files changed
+296
-10
No files matched your search
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user