forked from 1b-agent/opencadstudio-upstream
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:
1 parent
cdfb4ed632
commit
6dab5a7229
16 files changed
+1378
-26
No files matched your search
+88
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user