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:
parent
b0ee1c39bd
commit
e31dbcb5c4
8 changed files with 296 additions and 10 deletions
|
|
@ -132,6 +132,11 @@ pub(super) struct DocumentTab {
|
||||||
/// World-space anchor the current step's values are measured from. `None`
|
/// World-space anchor the current step's values are measured from. `None`
|
||||||
/// falls back to `App::last_point`.
|
/// falls back to `App::last_point`.
|
||||||
pub(super) dyn_anchor: Option<glam::Vec3>,
|
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).
|
/// Index of the field that TAB has focused (the one keystrokes edit).
|
||||||
pub(super) dyn_active: usize,
|
pub(super) dyn_active: usize,
|
||||||
pub(super) history: HistoryState,
|
pub(super) history: HistoryState,
|
||||||
|
|
@ -195,6 +200,8 @@ impl DocumentTab {
|
||||||
dyn_fields: Vec::new(),
|
dyn_fields: Vec::new(),
|
||||||
dyn_guide: crate::command::DynGuide::Polar,
|
dyn_guide: crate::command::DynGuide::Polar,
|
||||||
dyn_anchor: None,
|
dyn_anchor: None,
|
||||||
|
dyn_ref: None,
|
||||||
|
dyn_ref_screen: None,
|
||||||
dyn_active: 0,
|
dyn_active: 0,
|
||||||
history: HistoryState::default(),
|
history: HistoryState::default(),
|
||||||
active_layer: "0".to_string(),
|
active_layer: "0".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -2545,14 +2545,16 @@ impl OpenCADStudio {
|
||||||
// Project the step anchor (an explicit `dyn_anchor` or the
|
// Project the step anchor (an explicit `dyn_anchor` or the
|
||||||
// last point) so the dynamic-input overlay can place its
|
// last point) so the dynamic-input overlay can place its
|
||||||
// guide geometry and labels.
|
// guide geometry and labels.
|
||||||
let anchor = self.tabs[i].dyn_anchor.or(self.last_point);
|
let project = |bp: glam::Vec3| {
|
||||||
self.tabs[i].last_point_screen = anchor.map(|bp| {
|
|
||||||
let ndc = view_proj.project_point3(bp);
|
let ndc = view_proj.project_point3(bp);
|
||||||
iced::Point::new(
|
iced::Point::new(
|
||||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
(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
|
// Entity-pick previews (TRIM/EXTEND/FILLET…) compare the
|
||||||
// cursor against WCS document entities and return WCS wires.
|
// cursor against WCS document entities and return WCS wires.
|
||||||
|
|
@ -7590,6 +7592,7 @@ impl OpenCADStudio {
|
||||||
_ => crate::command::DynGuide::None,
|
_ => crate::command::DynGuide::None,
|
||||||
};
|
};
|
||||||
self.tabs[i].dyn_anchor = self.last_point;
|
self.tabs[i].dyn_anchor = self.last_point;
|
||||||
|
self.tabs[i].dyn_ref = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply an explicit per-step [`DynSpec`](crate::command::DynSpec): rebuild
|
/// 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::LastPoint => self.last_point,
|
||||||
crate::command::DynAnchor::Point(p) => Some(p),
|
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
|
/// Track cursor dwell over a selected entity's grip. Sets
|
||||||
|
|
@ -7783,15 +7787,29 @@ impl OpenCADStudio {
|
||||||
.or(self.last_point)
|
.or(self.last_point)
|
||||||
.unwrap_or(glam::Vec3::ZERO);
|
.unwrap_or(glam::Vec3::ZERO);
|
||||||
// Buffer value parsed as f32 (de-scaled by the role so a typed diameter
|
// 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 {
|
let val = |idx: usize, live: f32| -> f32 {
|
||||||
fields[idx]
|
match fields[idx]
|
||||||
.buffer
|
.buffer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| s.trim().replace(',', "."))
|
.map(|s| s.trim().replace(',', "."))
|
||||||
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32))
|
.and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32))
|
||||||
.map(|v| v / fields[idx].role.value_scale())
|
.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 dx = w.x - base.x;
|
||||||
let dy = w.y - base.y;
|
let dy = w.y - base.y;
|
||||||
|
|
@ -7812,6 +7830,28 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let comps: Vec<DynComponent> = fields.iter().map(|f| f.component).collect();
|
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
|
// DYN-on defaults to RELATIVE coordinates when a base point is set
|
||||||
// (see #26 / #35). The live cartesian fallback is the cursor
|
// (see #26 / #35). The live cartesian fallback is the cursor
|
||||||
// position relative to base; typed values are relative deltas.
|
// position relative to base; typed values are relative deltas.
|
||||||
|
|
|
||||||
|
|
@ -763,6 +763,7 @@ impl OpenCADStudio {
|
||||||
Some(overlay::dynamic_input_overlay(
|
Some(overlay::dynamic_input_overlay(
|
||||||
tab.last_cursor_screen,
|
tab.last_cursor_screen,
|
||||||
tab.last_point_screen,
|
tab.last_point_screen,
|
||||||
|
tab.dyn_ref_screen,
|
||||||
tab.dyn_guide,
|
tab.dyn_guide,
|
||||||
boxes,
|
boxes,
|
||||||
prompt,
|
prompt,
|
||||||
|
|
@ -4084,9 +4085,12 @@ fn dyn_component_value(
|
||||||
// in `dyn_resolve_point` so the live preview and the committed
|
// in `dyn_resolve_point` so the live preview and the committed
|
||||||
// coordinate use the same frame. See #35.
|
// coordinate use the same frame. See #35.
|
||||||
let has_base = base.is_some();
|
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 {
|
match f.component {
|
||||||
DynComponent::X if has_base => format!("{:.4}", dx),
|
DynComponent::X if has_base => format!("{:.4}", if wh { dx.abs() } else { dx }),
|
||||||
DynComponent::Y if has_base => format!("{:.4}", dy),
|
DynComponent::Y if has_base => format!("{:.4}", if wh { dy.abs() } else { dy }),
|
||||||
DynComponent::Z if has_base => "0.0000".to_string(),
|
DynComponent::Z if has_base => "0.0000".to_string(),
|
||||||
DynComponent::X => format!("{:.4}", w.x),
|
DynComponent::X => format!("{:.4}", w.x),
|
||||||
DynComponent::Y => format!("{:.4}", w.y),
|
DynComponent::Y => format!("{:.4}", w.y),
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,8 @@ pub enum DynRole {
|
||||||
Height,
|
Height,
|
||||||
/// Typed-only scale factor.
|
/// Typed-only scale factor.
|
||||||
Factor,
|
Factor,
|
||||||
/// Typed-only integer count.
|
/// Typed-only integer count. Reserved for upcoming command migrations.
|
||||||
|
#[allow(dead_code)]
|
||||||
Count,
|
Count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,12 +322,21 @@ pub enum DynGuide {
|
||||||
Radius,
|
Radius,
|
||||||
/// The two rectangle sides (width × height) from the anchor corner.
|
/// The two rectangle sides (width × height) from the anchor corner.
|
||||||
RectSides,
|
RectSides,
|
||||||
|
/// A line from the anchor, perpendicular to the reference line (anchor →
|
||||||
|
/// `DynSpec::ref_point`), reaching the cursor's perpendicular offset — the
|
||||||
|
/// measured semi-axis (ellipse minor). The value is that offset.
|
||||||
|
Perp,
|
||||||
|
/// Like `Perp` but drawn as a dimension: the measured segment is offset off
|
||||||
|
/// the edge with extension lines back to its endpoints (rectangle height).
|
||||||
|
PerpDim,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a step's values are measured from.
|
/// Where a step's values are measured from.
|
||||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
pub enum DynAnchor {
|
pub enum DynAnchor {
|
||||||
/// The previous committed point (`App::last_point`).
|
/// The previous committed point (`App::last_point`). Reserved — current
|
||||||
|
/// specs pass the anchor explicitly via `Point`.
|
||||||
|
#[allow(dead_code)]
|
||||||
LastPoint,
|
LastPoint,
|
||||||
/// An explicit world point.
|
/// An explicit world point.
|
||||||
Point(Vec3),
|
Point(Vec3),
|
||||||
|
|
@ -352,6 +362,9 @@ pub struct DynSpec {
|
||||||
pub anchor: DynAnchor,
|
pub anchor: DynAnchor,
|
||||||
pub fields: Vec<DynFieldSpec>,
|
pub fields: Vec<DynFieldSpec>,
|
||||||
pub guide: DynGuide,
|
pub guide: DynGuide,
|
||||||
|
/// Far end of a reference line through `anchor` (only used by
|
||||||
|
/// [`DynGuide::Perp`]); `None` otherwise.
|
||||||
|
pub ref_point: Option<Vec3>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Trait ─────────────────────────────────────────────────────────────────
|
// ── Trait ─────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ impl CadCommand for CircleCommand {
|
||||||
anchor: DynAnchor::Point(c),
|
anchor: DynAnchor::Point(c),
|
||||||
fields: vec![DynFieldSpec::new(DynRole::Radius)],
|
fields: vec![DynFieldSpec::new(DynRole::Radius)],
|
||||||
guide: DynGuide::Radius,
|
guide: DynGuide::Radius,
|
||||||
|
ref_point: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -288,6 +289,7 @@ impl CadCommand for CircleCDCommand {
|
||||||
anchor: DynAnchor::Point(c),
|
anchor: DynAnchor::Point(c),
|
||||||
fields: vec![DynFieldSpec::new(DynRole::Diameter)],
|
fields: vec![DynFieldSpec::new(DynRole::Diameter)],
|
||||||
guide: DynGuide::Radius,
|
guide: DynGuide::Radius,
|
||||||
|
ref_point: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,31 @@ impl CadCommand for EllipseCommand {
|
||||||
_ => None,
|
_ => 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 ─────────────────────────────────────────────────────
|
// ── 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 ───────────────────────────────────────────────────
|
// ── 3. Ellipse Arc mode ───────────────────────────────────────────────────
|
||||||
|
|
@ -487,6 +536,38 @@ impl CadCommand for EllipseArcCommand {
|
||||||
_ => None,
|
_ => 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 ──────────────────────────────────────────────────────
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,21 @@ impl CadCommand for RectCommand {
|
||||||
[a.x, pt.y, a.z],
|
[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) ──────────────────────────────
|
// ── Command: Rectangle — Rotated (RECT_ROT) ──────────────────────────────
|
||||||
|
|
@ -256,6 +271,29 @@ impl CadCommand for RectRotCommand {
|
||||||
_ => None,
|
_ => 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) ───────────────────────────────
|
// ── Command: Rectangle — Center (RECT_CEN) ───────────────────────────────
|
||||||
|
|
@ -320,6 +358,20 @@ impl CadCommand for RectCenCommand {
|
||||||
[c.x - hw, c.y + hh, c.z],
|
[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) ──────────────────────────────────
|
// ── 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> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Ok(n) = text.trim().parse::<u32>() {
|
if let Ok(n) = text.trim().parse::<u32>() {
|
||||||
if (3..=1024).contains(&n) {
|
if (3..=1024).contains(&n) {
|
||||||
|
|
@ -469,6 +535,20 @@ impl CadCommand for PolyCCommand {
|
||||||
Some(CmdResult::NeedPoint)
|
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 {
|
fn prompt(&self) -> String {
|
||||||
match self.step {
|
match self.step {
|
||||||
0 => format!("POLYGON C Enter number of sides <{}>:", self.sides),
|
0 => format!("POLYGON C Enter number of sides <{}>:", self.sides),
|
||||||
|
|
|
||||||
|
|
@ -1217,6 +1217,7 @@ pub struct DynBox {
|
||||||
pub fn dynamic_input_overlay<'a>(
|
pub fn dynamic_input_overlay<'a>(
|
||||||
cursor_screen: Point,
|
cursor_screen: Point,
|
||||||
base_screen: Option<Point>,
|
base_screen: Option<Point>,
|
||||||
|
ref_screen: Option<Point>,
|
||||||
guide: DynGuide,
|
guide: DynGuide,
|
||||||
boxes: Vec<DynBox>,
|
boxes: Vec<DynBox>,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
|
|
@ -1224,6 +1225,7 @@ pub fn dynamic_input_overlay<'a>(
|
||||||
canvas(DynInputCanvas {
|
canvas(DynInputCanvas {
|
||||||
cursor_screen,
|
cursor_screen,
|
||||||
base_screen,
|
base_screen,
|
||||||
|
ref_screen,
|
||||||
guide,
|
guide,
|
||||||
boxes,
|
boxes,
|
||||||
prompt,
|
prompt,
|
||||||
|
|
@ -1238,6 +1240,8 @@ struct DynInputCanvas {
|
||||||
/// Step anchor in viewport pixels (projected `dyn_anchor`). Guided layouts
|
/// Step anchor in viewport pixels (projected `dyn_anchor`). Guided layouts
|
||||||
/// (polar / radius / axis-delta) need it; `None` falls back to a cursor row.
|
/// (polar / radius / axis-delta) need it; `None` falls back to a cursor row.
|
||||||
base_screen: Option<Point>,
|
base_screen: Option<Point>,
|
||||||
|
/// Far end of the reference line (projected `dyn_ref`) — for `Perp`.
|
||||||
|
ref_screen: Option<Point>,
|
||||||
guide: DynGuide,
|
guide: DynGuide,
|
||||||
boxes: Vec<DynBox>,
|
boxes: Vec<DynBox>,
|
||||||
/// The active command's current prompt, drawn just above the boxes.
|
/// The active command's current prompt, drawn just above the boxes.
|
||||||
|
|
@ -1351,6 +1355,22 @@ impl DynInputCanvas {
|
||||||
}
|
}
|
||||||
let corner = Point { x: cursor.x, y: base.y }; // axis-delta elbow
|
let corner = Point { x: cursor.x, y: base.y }; // axis-delta elbow
|
||||||
|
|
||||||
|
// Perp / PerpDim: perpendicular direction to the reference line, the
|
||||||
|
// measured endpoint along it (`end`), and an offset dimension segment
|
||||||
|
// (`off_base`→`off_end`) drawn clear of the edge for PerpDim.
|
||||||
|
let perp_info = self.ref_screen.map(|r| {
|
||||||
|
let (ax, ay) = (r.x - base.x, r.y - base.y);
|
||||||
|
let al = (ax * ax + ay * ay).sqrt().max(1.0);
|
||||||
|
let (ux, uy) = (ax / al, ay / al); // axis unit (base → ref)
|
||||||
|
let (px, py) = (-uy, ux); // perpendicular unit
|
||||||
|
let signed = (cursor.x - base.x) * px + (cursor.y - base.y) * py;
|
||||||
|
let end = Point { x: base.x + px * signed, y: base.y + py * signed };
|
||||||
|
const OFF: f32 = 16.0; // dimension offset, away from the reference
|
||||||
|
let off_base = Point { x: base.x - ux * OFF, y: base.y - uy * OFF };
|
||||||
|
let off_end = Point { x: end.x - ux * OFF, y: end.y - uy * OFF };
|
||||||
|
(end, off_base, off_end)
|
||||||
|
});
|
||||||
|
|
||||||
// ── Guide geometry ──
|
// ── Guide geometry ──
|
||||||
match self.guide {
|
match self.guide {
|
||||||
DynGuide::Polar => {
|
DynGuide::Polar => {
|
||||||
|
|
@ -1383,6 +1403,34 @@ impl DynInputCanvas {
|
||||||
});
|
});
|
||||||
frame.stroke(&line, Self::dotted());
|
frame.stroke(&line, Self::dotted());
|
||||||
}
|
}
|
||||||
|
DynGuide::Perp => {
|
||||||
|
if let Some((end, _, _)) = perp_info {
|
||||||
|
// The measured semi-axis: anchor → perpendicular endpoint.
|
||||||
|
let line = canvas::Path::new(|p| {
|
||||||
|
p.move_to(base);
|
||||||
|
p.line_to(end);
|
||||||
|
});
|
||||||
|
frame.stroke(&line, Self::dotted());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynGuide::PerpDim => {
|
||||||
|
if let Some((end, ob, oe)) = perp_info {
|
||||||
|
// Dimension segment offset off the edge, with extension
|
||||||
|
// lines back to the two measured corners.
|
||||||
|
let dim = canvas::Path::new(|p| {
|
||||||
|
p.move_to(ob);
|
||||||
|
p.line_to(oe);
|
||||||
|
});
|
||||||
|
frame.stroke(&dim, Self::dotted());
|
||||||
|
let ext = canvas::Path::new(|p| {
|
||||||
|
p.move_to(base);
|
||||||
|
p.line_to(ob);
|
||||||
|
p.move_to(end);
|
||||||
|
p.line_to(oe);
|
||||||
|
});
|
||||||
|
frame.stroke(&ext, Self::dotted());
|
||||||
|
}
|
||||||
|
}
|
||||||
DynGuide::AxisDelta | DynGuide::RectSides => {
|
DynGuide::AxisDelta | DynGuide::RectSides => {
|
||||||
// Dotted legs from the anchor along its axes to the cursor.
|
// Dotted legs from the anchor along its axes to the cursor.
|
||||||
let legs = canvas::Path::new(|p| {
|
let legs = canvas::Path::new(|p| {
|
||||||
|
|
@ -1422,6 +1470,17 @@ impl DynInputCanvas {
|
||||||
x: corner.x + 18.0,
|
x: corner.x + 18.0,
|
||||||
y: (base.y + cursor.y) * 0.5,
|
y: (base.y + cursor.y) * 0.5,
|
||||||
},
|
},
|
||||||
|
// Perpendicular measure: on the measured segment / dim line.
|
||||||
|
_ if matches!(self.guide, DynGuide::Perp | DynGuide::PerpDim)
|
||||||
|
&& perp_info.is_some() =>
|
||||||
|
{
|
||||||
|
let (end, ob, oe) = perp_info.unwrap();
|
||||||
|
if self.guide == DynGuide::PerpDim {
|
||||||
|
Point { x: (ob.x + oe.x) * 0.5 + 8.0, y: (ob.y + oe.y) * 0.5 }
|
||||||
|
} else {
|
||||||
|
Point { x: (base.x + end.x) * 0.5 + 8.0, y: (base.y + end.y) * 0.5 }
|
||||||
|
}
|
||||||
|
}
|
||||||
// Distance / Radius / Diameter and anything else ride the line.
|
// Distance / Radius / Diameter and anything else ride the line.
|
||||||
_ => Point {
|
_ => Point {
|
||||||
x: base.x + dx * len * 0.5 + nx * 16.0,
|
x: base.x + dx * len * 0.5 + nx * 16.0,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue