mirror of
https://github.com/aphrody-labs/cadcraft.git
synced 2026-10-13 19:26:54 +00:00
UI: hot grips (click-to-edit, Space cycles stretch/move/rotate/scale/mirror), paper-space sheet display, new-layout buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3da0219ac9
commit
8188c7ed0a
3 files changed
+225
-51
No files matched your search
@@ -35,6 +35,46 @@ impl Xf {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct HotGrip {
|
||||
pub handle: Handle,
|
||||
pub index: usize,
|
||||
pub base: Vec2,
|
||||
pub mode: cadcraft_engine::grips::GripMode,
|
||||
}
|
||||
|
||||
impl HotGrip {
|
||||
pub fn label(&self) -> &'static str {
|
||||
use cadcraft_engine::grips::GripMode::*;
|
||||
match self.mode {
|
||||
Stretch => "** STRETCH ** Specify stretch point or [Base point/Copy/Undo/eXit]:",
|
||||
Move => "** MOVE ** Specify move point or [Base point/Copy/Undo/eXit]:",
|
||||
Rotate => "** ROTATE ** Specify rotation angle or [Base point/Copy/Undo/Reference/eXit]:",
|
||||
Scale => "** SCALE ** Specify scale factor or [Base point/Copy/Undo/Reference/eXit]:",
|
||||
Mirror => "** MIRROR ** Specify second point or [Base point/Copy/Undo/eXit]:",
|
||||
}
|
||||
}
|
||||
pub fn next_mode(&mut self) {
|
||||
use cadcraft_engine::grips::GripMode::*;
|
||||
self.mode = match self.mode {
|
||||
Stretch => Move,
|
||||
Move => Rotate,
|
||||
Rotate => Scale,
|
||||
Scale => Mirror,
|
||||
Mirror => Stretch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a hot grip at `to` (one undo step) and clear it.
|
||||
pub fn apply_hot_grip(app: &mut CadApp, to: Vec2) {
|
||||
if let Some(g) = app.canvas.hot_grip.take()
|
||||
&& let Err(e) = app.session.grip_edit(g.handle, g.index, to, g.mode)
|
||||
{
|
||||
app.session.echo(e.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CanvasState {
|
||||
pub list: Option<DisplayList>,
|
||||
@@ -57,6 +97,8 @@ pub struct CanvasState {
|
||||
pub cursor: Option<Vec2>,
|
||||
pub polar_angle: Option<f64>,
|
||||
pan_last: Option<Pos2>,
|
||||
/// A hot (clicked) grip being dragged: entity, grip index, grip position, mode.
|
||||
pub hot_grip: Option<HotGrip>,
|
||||
/// Zoom to extents once the canvas size is known (after opening a drawing).
|
||||
pub zoom_pending: bool,
|
||||
pub build_ms: f64,
|
||||
@@ -371,8 +413,9 @@ fn draw_snap_marker(p: &egui::Painter, at: Pos2, hit: &SnapHit) {
|
||||
/// The rubber-band and snap-adjusted cursor point for the current prompt.
|
||||
fn effective_point(app: &mut CadApp, raw: Vec2, xf: &Xf) -> Vec2 {
|
||||
let prompt = app.session.current_prompt();
|
||||
let wants_point = prompt.as_ref().is_some_and(|p| p.accept.point && !p.accept.select);
|
||||
let base = prompt.as_ref().and_then(|p| p.base);
|
||||
let hot = app.canvas.hot_grip;
|
||||
let wants_point = hot.is_some() || prompt.as_ref().is_some_and(|p| p.accept.point && !p.accept.select);
|
||||
let base = hot.map(|g| g.base).or_else(|| prompt.as_ref().and_then(|p| p.base));
|
||||
let s = app.session.settings.clone();
|
||||
app.canvas.snap = None;
|
||||
app.canvas.polar_angle = None;
|
||||
@@ -491,9 +534,15 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
&& let Some(p) = app.canvas.cursor
|
||||
{
|
||||
if app.session.running.is_some() {
|
||||
app.canvas.hot_grip = None;
|
||||
if let Err(e) = app.session.input(Input::Point(p)) {
|
||||
app.session.echo(e.to_string());
|
||||
}
|
||||
} else if app.canvas.hot_grip.is_some() {
|
||||
apply_hot_grip(app, p);
|
||||
} else if let Some(g) = grip_at(app, &xf, hover_pos) {
|
||||
app.canvas.hot_grip = Some(g);
|
||||
app.session.echo(g.label());
|
||||
} else if let Err(e) = app.session.idle_click(raw_world.unwrap_or(p), mods.shift) {
|
||||
app.session.echo(e.to_string());
|
||||
}
|
||||
@@ -526,10 +575,31 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
|
||||
// ---------- paint ----------
|
||||
painter.rect_filled(rect, 0.0, t.canvas);
|
||||
draw_grid(app, &painter, &xf);
|
||||
let t0 = crate::now_ms();
|
||||
ensure_list(app, 1.0 / scale);
|
||||
let bg = Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b());
|
||||
let sheet = app.canvas.list.as_ref().and_then(|l| l.sheet);
|
||||
let bg = match sheet {
|
||||
Some(sh) => {
|
||||
// Paper space: grey surround, the sheet with a shadow, the printable area dashed.
|
||||
painter.rect_filled(rect, 0.0, Color32::from_rgb(0x50, 0x57, 0x63));
|
||||
let a = xf.to_screen(Vec2::new(0.0, sh.size.y));
|
||||
let b = xf.to_screen(Vec2::new(sh.size.x, 0.0));
|
||||
let paper = Rect::from_two_pos(a, b);
|
||||
painter.rect_filled(paper.translate(vec2(5.0, 5.0)), 0.0, Color32::from_black_alpha(110));
|
||||
painter.rect_filled(paper, 0.0, Color32::WHITE);
|
||||
let pa = Rect::from_two_pos(
|
||||
xf.to_screen(Vec2::new(sh.printable.min.x, sh.printable.max.y)),
|
||||
xf.to_screen(Vec2::new(sh.printable.max.x, sh.printable.min.y)),
|
||||
);
|
||||
let pts = [pa.left_top(), pa.right_top(), pa.right_bottom(), pa.left_bottom(), pa.left_top()];
|
||||
painter.extend(Shape::dashed_line(&pts, Stroke::new(1.0, Color32::from_gray(150)), 4.0, 4.0));
|
||||
Rgb(255, 255, 255)
|
||||
}
|
||||
None => {
|
||||
draw_grid(app, &painter, &xf);
|
||||
Rgb(t.canvas.r(), t.canvas.g(), t.canvas.b())
|
||||
}
|
||||
};
|
||||
let sel = app.session.selection();
|
||||
if app.canvas.gpu.is_some() {
|
||||
draw_list_gpu(&mut app.canvas, &painter, &xf, bg, app.session.settings.lwdisplay);
|
||||
@@ -552,17 +622,53 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
let gs = app.session.settings.gripsize as f32;
|
||||
for h in sel.iter().take(200) {
|
||||
if let Some(e) = st.doc.entity(*h) {
|
||||
for g in e.kind.grips() {
|
||||
for (gi, g) in e.kind.grips().into_iter().enumerate() {
|
||||
let sp = xf.to_screen(g);
|
||||
if rect.contains(sp) {
|
||||
let r = Rect::from_center_size(sp, vec2(gs * 2.0, gs * 2.0));
|
||||
painter.rect_filled(r, 0.0, t.grip);
|
||||
let hot = app.canvas.hot_grip.is_some_and(|hg| hg.handle == *h && hg.index == gi);
|
||||
let warm = !hot && hover_pos.is_some_and(|hp| r.expand(2.0).contains(hp));
|
||||
painter.rect_filled(
|
||||
r,
|
||||
0.0,
|
||||
if hot {
|
||||
t.grip_hot
|
||||
} else if warm {
|
||||
Color32::from_rgb(0xff, 0x7f, 0x9f)
|
||||
} else {
|
||||
t.grip
|
||||
},
|
||||
);
|
||||
painter.rect_stroke(r, 0.0, Stroke::new(1.0, Color32::from_rgb(0x10, 0x30, 0x80)), egui::StrokeKind::Middle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hot grip preview.
|
||||
if let (Some(g), Some(c), Ok(st)) = (app.canvas.hot_grip, app.canvas.cursor, app.session.state())
|
||||
&& let Some(e) = st.doc.entity(g.handle)
|
||||
{
|
||||
let k = match cadcraft_engine::grips::mode_matrix(g.mode, g.base, c) {
|
||||
Some(m) => {
|
||||
let mut k = e.kind.clone();
|
||||
k.transform(&m);
|
||||
Some(k)
|
||||
}
|
||||
None if g.mode == cadcraft_engine::grips::GripMode::Stretch => cadcraft_engine::grips::stretch_grip(&e.kind, g.index, c),
|
||||
None => None,
|
||||
};
|
||||
if let Some(k) = k {
|
||||
let ent = cadcraft_doc::Entity { handle: Handle(0), common: e.common.clone(), kind: k };
|
||||
let list = cadcraft_render::build_entities(
|
||||
&st.doc,
|
||||
std::iter::once(&ent),
|
||||
&cadcraft_render::Options { tolerance: 0.5 / scale, min_dash: 2.0 / scale, ..Default::default() },
|
||||
);
|
||||
draw_list(&painter, &xf, &list, bg, false);
|
||||
}
|
||||
painter.extend(Shape::dashed_line(&[xf.to_screen(g.base), xf.to_screen(c)], Stroke::new(1.0, t.text_dim), 4.0, 3.0));
|
||||
}
|
||||
// Rubber band preview of the active command.
|
||||
if let (Some(c), true) = (app.canvas.cursor, app.session.running.is_some()) {
|
||||
let ents = app.session.preview(c);
|
||||
@@ -607,7 +713,7 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
if app.ui.show_ucs_icon {
|
||||
draw_ucs_icon(&painter, rect);
|
||||
}
|
||||
if app.ui.show_viewcube {
|
||||
if app.ui.show_viewcube && sheet.is_none() {
|
||||
draw_viewcube(app, ui, rect);
|
||||
}
|
||||
viewport_label(&painter, rect);
|
||||
@@ -726,3 +832,23 @@ fn draw_viewcube(app: &mut CadApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
p.rect_filled(pill, 8.0, Color32::from_rgb(0x48, 0x50, 0x5c));
|
||||
p.text(pill.center(), egui::Align2::CENTER_CENTER, "WCS ⌄", crate::theme::small(), t.text_dim);
|
||||
}
|
||||
|
||||
/// The grip of a selected object under the cursor, if any.
|
||||
fn grip_at(app: &CadApp, xf: &Xf, hover: Option<Pos2>) -> Option<HotGrip> {
|
||||
let hp = hover?;
|
||||
let st = app.session.state().ok()?;
|
||||
let gs = app.session.settings.gripsize as f32 + 3.0;
|
||||
for h in app.session.selection().iter().take(200) {
|
||||
let e = st.doc.entity(*h)?;
|
||||
if st.doc.layer(&e.common.layer).is_some_and(|l| l.locked) {
|
||||
continue;
|
||||
}
|
||||
for (i, g) in e.kind.grips().into_iter().enumerate() {
|
||||
let sp = xf.to_screen(g);
|
||||
if (sp.x - hp.x).abs() <= gs && (sp.y - hp.y).abs() <= gs {
|
||||
return Some(HotGrip { handle: *h, index: i, base: g, mode: cadcraft_engine::grips::GripMode::Stretch });
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -183,7 +183,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
let resp = ui.interact(br, ui.id().with(("sb", tip)), Sense::click());
|
||||
icons::paint(&p, br.shrink(2.0), icon, false);
|
||||
if resp.on_hover_text(tip).clicked() && icon == Icon::Plus {
|
||||
app.set_status("New layout: coming soon");
|
||||
let _ = app.run("layout.new", json!({}));
|
||||
}
|
||||
x += 22.0;
|
||||
}
|
||||
@@ -218,7 +218,14 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
x += w + 2.0;
|
||||
if i == 0 {
|
||||
let pr = Rect::from_min_size(pos2(x, r.top() + 5.0), vec2(16.0, 16.0));
|
||||
let presp = ui.interact(pr, ui.id().with("layout_plus"), Sense::click());
|
||||
icons::paint(&p, pr.shrink(2.0), Icon::Plus, false);
|
||||
if presp.on_hover_text("New layout").clicked()
|
||||
&& let Ok(v) = app.run("layout.new", json!({}))
|
||||
&& let Some(n) = v.get("name").and_then(serde_json::Value::as_str)
|
||||
{
|
||||
switch = Some(n.to_string());
|
||||
}
|
||||
x += 22.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,40 @@ pub fn keyboard(app: &mut CadApp, ctx: &egui::Context) {
|
||||
return;
|
||||
}
|
||||
let events = ctx.input(|i| i.events.clone());
|
||||
// A hot grip takes Space/Enter (cycle mode, or apply a typed point) and Escape.
|
||||
if app.canvas.hot_grip.is_some() {
|
||||
for ev in &events {
|
||||
match ev {
|
||||
egui::Event::Text(t) if t != " " => app.cmd.buffer.push_str(t),
|
||||
egui::Event::Key { key: Key::Backspace, pressed: true, .. } => {
|
||||
app.cmd.buffer.pop();
|
||||
}
|
||||
egui::Event::Key { key: Key::Escape, pressed: true, .. } => {
|
||||
app.canvas.hot_grip = None;
|
||||
app.cmd.buffer.clear();
|
||||
app.session.echo("*Cancel*");
|
||||
}
|
||||
egui::Event::Key { key: Key::Enter | Key::Space, pressed: true, .. } => {
|
||||
let typed = std::mem::take(&mut app.cmd.buffer);
|
||||
if typed.trim().is_empty() {
|
||||
if let Some(g) = app.canvas.hot_grip.as_mut() {
|
||||
g.next_mode();
|
||||
let l = g.label();
|
||||
app.session.echo(l);
|
||||
}
|
||||
} else {
|
||||
let base = app.canvas.hot_grip.map(|g| g.base).unwrap_or_default();
|
||||
match cadcraft_engine::prompt::parse_point(&typed, base) {
|
||||
Some(p) => crate::canvas::apply_hot_grip(app, p),
|
||||
None => app.session.echo("Requires a point (x,y, @dx,dy or @d<a)."),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let text_prompt = app.session.current_prompt().is_some_and(|p| p.accept.text && !p.accept.point && !p.accept.number);
|
||||
for ev in events {
|
||||
match ev {
|
||||
@@ -134,62 +168,69 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui, canvas: Rect) {
|
||||
let mut x = bar.left() + 30.0;
|
||||
let prompt = app.session.current_prompt();
|
||||
let font = crate::theme::body();
|
||||
match &prompt {
|
||||
Some(pr) => {
|
||||
let name = app.session.running.as_ref().map(|r| r.id.to_ascii_uppercase()).unwrap_or_default();
|
||||
let g = p.layout_no_wrap(format!("{name} "), font.clone(), t.text_faint);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text_faint);
|
||||
x += gw;
|
||||
let g = p.layout_no_wrap(pr.message.clone(), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
if !pr.keywords.is_empty() {
|
||||
let g = p.layout_no_wrap(if pr.message.is_empty() { " [".into() } else { " or [".into() }, font.clone(), t.text);
|
||||
if let Some(g) = app.canvas.hot_grip {
|
||||
let gl = p.layout_no_wrap(g.label().to_string(), font.clone(), t.text);
|
||||
let gw = gl.size().x;
|
||||
p.galley(pos2(x, bar.center().y - gl.size().y / 2.0), gl, t.text);
|
||||
x += gw + 6.0;
|
||||
} else {
|
||||
match &prompt {
|
||||
Some(pr) => {
|
||||
let name = app.session.running.as_ref().map(|r| r.id.to_ascii_uppercase()).unwrap_or_default();
|
||||
let g = p.layout_no_wrap(format!("{name} "), font.clone(), t.text_faint);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text_faint);
|
||||
x += gw;
|
||||
let g = p.layout_no_wrap(pr.message.clone(), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
let kws = pr.keywords.clone();
|
||||
for (i, k) in kws.iter().enumerate() {
|
||||
let g = p.layout_no_wrap(k.clone(), font.clone(), Color32::from_rgb(0x8f, 0xc1, 0xff));
|
||||
let r = Rect::from_min_size(pos2(x, bar.top() + 3.0), vec2(g.size().x, h - 6.0));
|
||||
let resp = ui.interact(r, ui.id().with(("kw", i)), Sense::click());
|
||||
if resp.hovered() {
|
||||
p.rect_filled(r, 2.0, Color32::from_rgb(0x2f, 0x5e, 0xa8));
|
||||
}
|
||||
if !pr.keywords.is_empty() {
|
||||
let g = p.layout_no_wrap(if pr.message.is_empty() { " [".into() } else { " or [".into() }, font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, Color32::from_rgb(0x8f, 0xc1, 0xff));
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
if resp.clicked() {
|
||||
let _ = app.session.input(Input::Keyword(k.clone()));
|
||||
}
|
||||
if i + 1 < kws.len() {
|
||||
let g = p.layout_no_wrap("/".into(), font.clone(), t.text);
|
||||
let kws = pr.keywords.clone();
|
||||
for (i, k) in kws.iter().enumerate() {
|
||||
let g = p.layout_no_wrap(k.clone(), font.clone(), Color32::from_rgb(0x8f, 0xc1, 0xff));
|
||||
let r = Rect::from_min_size(pos2(x, bar.top() + 3.0), vec2(g.size().x, h - 6.0));
|
||||
let resp = ui.interact(r, ui.id().with(("kw", i)), Sense::click());
|
||||
if resp.hovered() {
|
||||
p.rect_filled(r, 2.0, Color32::from_rgb(0x2f, 0x5e, 0xa8));
|
||||
}
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, Color32::from_rgb(0x8f, 0xc1, 0xff));
|
||||
x += gw;
|
||||
if resp.clicked() {
|
||||
let _ = app.session.input(Input::Keyword(k.clone()));
|
||||
}
|
||||
if i + 1 < kws.len() {
|
||||
let g = p.layout_no_wrap("/".into(), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
}
|
||||
}
|
||||
let g = p.layout_no_wrap("]".into(), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
}
|
||||
let g = p.layout_no_wrap("]".into(), font.clone(), t.text);
|
||||
if let Some(d) = &pr.default {
|
||||
let g = p.layout_no_wrap(format!(" <{d}>"), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
}
|
||||
let g = p.layout_no_wrap(": ".into(), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
}
|
||||
if let Some(d) = &pr.default {
|
||||
let g = p.layout_no_wrap(format!(" <{d}>"), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
}
|
||||
let g = p.layout_no_wrap(": ".into(), font.clone(), t.text);
|
||||
let gw = g.size().x;
|
||||
p.galley(pos2(x, bar.center().y - g.size().y / 2.0), g, t.text);
|
||||
x += gw;
|
||||
}
|
||||
None => {
|
||||
if app.cmd.buffer.is_empty() {
|
||||
p.text(pos2(x, bar.center().y), egui::Align2::LEFT_CENTER, "Type a command", egui::FontId::proportional(12.5), t.text_faint);
|
||||
None => {
|
||||
if app.cmd.buffer.is_empty() {
|
||||
p.text(pos2(x, bar.center().y), egui::Align2::LEFT_CENTER, "Type a command", egui::FontId::proportional(12.5), t.text_faint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user