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

+88 -1
View File
@@ -946,7 +946,7 @@ impl canvas::Program<Message> for GridCanvas {
}
}
pub fn selection_overlay<'a>(
pub(crate) fn selection_overlay<'a>(
selection: Arc<RefCell<SelectionState>>,
snap: Option<(Point, SnapType)>,
snap_ext_base: Option<Point>,
@@ -979,6 +979,7 @@ pub fn selection_overlay<'a>(
constraint_cursor_badge: Option<String>,
authoring_visibility: Option<(Point, String, iced::Rectangle)>,
authoring_points: Vec<(Point, Point, String, iced::Rectangle)>,
authoring_dimensions: Vec<AuthoringDimension>,
) -> Element<'a, Message> {
canvas(SelectionCanvas {
selection,
@@ -1009,12 +1010,21 @@ pub fn selection_overlay<'a>(
constraint_cursor_badge,
authoring_visibility,
authoring_points,
authoring_dimensions,
})
.width(Length::Fill)
.height(Length::Fill)
.into()
}
pub(crate) struct AuthoringDimension {
pub lines: Vec<[Point; 2]>,
pub dimensions: Vec<[Point; 2]>,
pub labels: Vec<(Point, String)>,
pub grips: Vec<(Point, Option<Point>)>,
pub clip: iced::Rectangle,
}
struct SelectionCanvas {
selection: Arc<RefCell<SelectionState>>,
snap: Option<(Point, SnapType)>,
@@ -1087,6 +1097,7 @@ struct SelectionCanvas {
constraint_cursor_badge: Option<String>,
authoring_visibility: Option<(Point, String, iced::Rectangle)>,
authoring_points: Vec<(Point, Point, String, iced::Rectangle)>,
authoring_dimensions: Vec<AuthoringDimension>,
}
/// The move gizmo: an arrow per axis (X red, Y green, Z blue) from the
@@ -2351,6 +2362,82 @@ impl canvas::Program<Message> for SelectionCanvas {
});
});
}
for dimension in &self.authoring_dimensions {
frame.with_clip(dimension.clip, |frame| {
let color = theme.palette().primary.base.color;
for [a, b] in &dimension.lines {
frame.stroke(
&canvas::Path::line(*a, *b),
canvas::Stroke::default().with_color(color),
);
}
for [a, b] in &dimension.dimensions {
let dx = b.x - a.x;
let dy = b.y - a.y;
let length = dx.hypot(dy).max(1.0);
let (x, y) = (dx / length, dy / length);
for (point, sign) in [(a, 1.0), (b, -1.0)] {
let tip = Point::new(point.x + sign * 7.0 * x, point.y + sign * 7.0 * y);
let arrow = canvas::Path::new(|path| {
path.move_to(*point);
path.line_to(Point::new(tip.x - 2.5 * y, tip.y + 2.5 * x));
path.line_to(Point::new(tip.x + 2.5 * y, tip.y - 2.5 * x));
path.close();
});
frame.fill(&arrow, color);
}
}
for (point, label) in &dimension.labels {
// Keep diagonal dimension lines from crossing the label.
let background = Color::from_rgba(
self.crosshair_bg[0],
self.crosshair_bg[1],
self.crosshair_bg[2],
self.crosshair_bg[3],
);
frame.fill(
&canvas::Path::rectangle(
Point::new(point.x + 2.0, point.y - 19.0),
iced::Size::new(label.chars().count() as f32 * 8.0 + 6.0, 19.0),
),
background,
);
frame.fill_text(canvas::Text {
content: label.clone(),
position: Point::new(point.x + 5.0, point.y - 17.0),
color,
size: iced::Pixels(13.0),
..Default::default()
});
}
for (point, base) in &dimension.grips {
let marker = if let Some(base) = base {
let dx = point.x - base.x;
let dy = point.y - base.y;
let length = dx.hypot(dy).max(1.0);
let (x, y) = (dx / length, dy / length);
canvas::Path::new(|path| {
path.move_to(Point::new(point.x + 5.0 * x, point.y + 5.0 * y));
path.line_to(Point::new(
point.x - 4.0 * x - 4.0 * y,
point.y - 4.0 * y + 4.0 * x,
));
path.line_to(Point::new(
point.x - 4.0 * x + 4.0 * y,
point.y - 4.0 * y - 4.0 * x,
));
path.close();
})
} else {
canvas::Path::rectangle(
Point::new(point.x - 3.0, point.y - 3.0),
iced::Size::new(6.0, 6.0),
)
};
frame.fill(&marker, color);
}
});
}
// Draw + hover + tooltip all read the memoised entries' precomputed
// `top_left`/`size` (one shared `hovered` lookup below) — no offset
// recompute, no label clones on this path.
+8 -16
View File
@@ -208,8 +208,8 @@ fn catalog(
column(entries.iter().map(|(label, icon)| {
let visibility = tab == Tab::Parameters && *label == "Visibility";
let point = tab == Tab::Parameters && *label == "Point";
let enabled =
(visibility && visibility_error.is_none()) || (point && point_error.is_none());
let distance = tab == Tab::Parameters && matches!(*label, "Linear" | "Polar" | "XY");
let enabled = (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
let base = if enabled {
icons::semantic(icon, 28.0)
} else {
@@ -232,20 +232,8 @@ fn catalog(
base
};
let message = enabled.then(|| Message::RibbonToolClick {
tool_id: if point {
"BPARAMETER_POINT"
} else {
"BPARAMETER_VISIBILITY"
}
.into(),
event: crate::modules::ModuleEvent::Command(
if point {
"BPARAMETER POINT"
} else {
"BPARAMETER VISIBILITY"
}
.into(),
),
tool_id: format!("BPARAMETER_{}", label.to_ascii_uppercase()),
event: crate::modules::ModuleEvent::Command(format!("BPARAMETER {}", label.to_ascii_uppercase())),
});
let description = if visibility {
crate::t!(visibility_error
@@ -254,6 +242,8 @@ fn catalog(
} else if point {
crate::t!(point_error.unwrap_or("Place a point parameter and its position label."))
.into_owned()
} else if distance {
crate::t!(point_error.unwrap_or("Place the base point, endpoint, and dimension label. Creates one end grip; associate an action to change geometry.")).into_owned()
} else {
unavailable.to_string()
};
@@ -262,6 +252,8 @@ fn catalog(
container(entry).id("authoring-tool-visibility").into()
} else if point {
container(entry).id("authoring-tool-point").into()
} else if distance {
container(entry).id(match *label { "Linear" => "authoring-tool-linear", "Polar" => "authoring-tool-polar", _ => "authoring-tool-xy" }).into()
} else {
entry
}