feat(block-editor): wire Linear, Polar and XY parameter placement

Enable the three existing palette rows and BPARAMETER choices. Pick a base,
endpoint and label location to create the appropriate native parameter, one
end grip and its location expressions in the shared authoring graph.

Render editor dimensions and labels, preserve them when reframing the block,
and include them in editor extents. Reuse local undo/redo, save/discard and
native DWG/DXF persistence. Reject incomplete or invalid placement without
changing document data or history.

This milestone implements parameter placement. Action association/evaluation,
parameter property editing, alternate grip counts and parameter sets remain
separate work.

Validation: focused placement and palette interaction checks; Block Editor and
dock regressions; native append and DWG/DXF round trips in all 17 research
fixtures, preserving existing graph data and source files; app-view renders.
This commit is contained in:
Karim Jerbi committed 2026-10-15 16:00:15 +01:00
1 parent cdfb4ed632
commit 6dab5a7229
16 files changed
+1378 -26

No files matched your search

@@ -0,0 +1,129 @@
//! Three-pick authoring shared by Linear, Polar and XY parameters.
use crate::command::{CadCommand, CmdResult};
use crate::scene::{
block_distance_parameter::{self, Kind},
WireModel,
};
use codec::{types::Vector3, Handle};
use glam::DVec3;
pub struct DistanceParameterCommand {
document_id: u64,
definition: Handle,
kind: Kind,
base: Option<DVec3>,
end: Option<DVec3>,
}
impl DistanceParameterCommand {
pub(crate) fn new(document_id: u64, definition: Handle, kind: Kind) -> Self {
Self {
document_id,
definition,
kind,
base: None,
end: None,
}
}
}
impl CadCommand for DistanceParameterCommand {
fn name(&self) -> &'static str {
"BPARAMETER"
}
fn prompt(&self) -> String {
let step = if self.base.is_none() {
"Specify parameter base point:"
} else if self.end.is_none() {
"Specify parameter endpoint:"
} else {
"Specify parameter label location:"
};
crate::tf!(
"BPARAMETER {} {}",
crate::t!(self.kind.name()),
crate::t!(step)
)
.into_owned()
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
if !point.is_finite() {
return CmdResult::ReportError(
crate::t!("Specify a finite parameter location.").into_owned(),
);
}
let Some(base) = self.base else {
self.base = Some(point);
return CmdResult::NeedPoint;
};
let vector = |p: DVec3| Vector3::new(p.x, p.y, p.z);
if let Some(error) = block_distance_parameter::points_error(
self.kind,
vector(base),
vector(self.end.unwrap_or(point)),
vector(point),
) {
return CmdResult::ReportError(crate::t!(error).into_owned());
}
let Some(end) = self.end else {
self.end = Some(point);
return CmdResult::NeedPoint;
};
CmdResult::PlaceBlockDistanceParameter {
document_id: self.document_id,
definition: self.definition,
kind: self.kind,
base,
end,
label: point,
}
}
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
let Some(base) = self.base.filter(|_| point.is_finite()) else {
return vec![];
};
let end = self.end.unwrap_or(point);
let mut lines = vec![[base, end]];
if self.kind == Kind::XY {
lines = vec![
[base, DVec3::new(end.x, base.y, base.z)],
[base, DVec3::new(base.x, end.y, base.z)],
];
}
if self.end.is_some() {
if self.kind == Kind::XY {
let x = DVec3::new(base.x, point.y, base.z);
let y = DVec3::new(point.x, base.y, base.z);
lines.extend([
[base, x],
[x, DVec3::new(end.x, point.y, base.z)],
[base, y],
[y, DVec3::new(point.x, end.y, base.z)],
]);
} else {
let d = end - base;
let n = DVec3::new(-d.y, d.x, 0.0).normalize_or_zero();
let offset = n * (point - base).dot(n);
lines = vec![
[base, base + offset],
[base + offset, end + offset],
[end + offset, end],
];
}
}
lines
.into_iter()
.map(|line| {
WireModel::solid_f64(
"preview_distance_parameter".into(),
line.into_iter().map(|point| point.to_array()).collect(),
WireModel::CYAN,
false,
)
})
.collect()
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+1
View File
@@ -7,6 +7,7 @@ use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem,
pub mod visibility_parameter;
pub mod point_parameter;
pub mod distance_parameter;
pub struct BlockEditorModule;
@@ -27,8 +27,10 @@ impl CadCommand for VisibilityParameterCommand {
fn prompt(&self) -> String {
if self.choosing_type {
crate::t!("BPARAMETER Enter parameter type [Point/Visibility] <Visibility>:")
.into_owned()
crate::t!(
"BPARAMETER Enter parameter type [Point/Linear/Polar/XY/Visibility] <Visibility>:"
)
.into_owned()
} else {
crate::t!("BPARAMETER Specify visibility parameter location:").into_owned()
}
@@ -38,6 +40,9 @@ impl CadCommand for VisibilityParameterCommand {
if self.choosing_type {
vec![
CmdOption::new(crate::t!("Point").as_ref(), "P"),
CmdOption::new(crate::t!("Linear").as_ref(), "L"),
CmdOption::new(crate::t!("Polar").as_ref(), "PO"),
CmdOption::new(crate::t!("XY").as_ref(), "XY"),
CmdOption::new(crate::t!("Visibility").as_ref(), "V"),
]
} else {
@@ -60,16 +65,17 @@ impl CadCommand for VisibilityParameterCommand {
.to_ascii_uppercase()
.as_str()
{
"L" | "LINEAR" => CmdResult::Dispatch("BPARAMETER LINEAR".into()),
"PO" | "POLAR" => CmdResult::Dispatch("BPARAMETER POLAR".into()),
"XY" => CmdResult::Dispatch("BPARAMETER XY".into()),
"P" | "POINT" => CmdResult::Dispatch("BPARAMETER POINT".into()),
"" | "V" | "VISIBILITY" => {
self.choosing_type = false;
CmdResult::NeedPoint
}
_ => CmdResult::ReportError(
crate::t!(
"Only Point and Visibility parameter placement is currently supported."
)
.into_owned(),
crate::t!("Supported parameter types: Point, Linear, Polar, XY, Visibility.")
.into_owned(),
),
},
)