2026-03-22 22:44:32 +03:00
|
|
|
|
use super::document::DocumentTab;
|
|
|
|
|
|
use super::helpers::grid_plane_from_camera;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
use super::history::history_dropdown_labels;
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
use super::document::{DynComponent, DynFieldEntry};
|
2026-05-23 17:16:15 +03:00
|
|
|
|
use super::{Message, OpenCADStudio};
|
2026-05-01 12:16:46 +03:00
|
|
|
|
use crate::scene::grip::{grips_to_screen, grips_to_screen_paper};
|
2026-05-28 10:03:01 +03:00
|
|
|
|
use crate::scene::viewport_pane::ViewportPane;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
use crate::ui::overlay;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
use iced::widget::{
|
2026-06-02 20:25:50 +03:00
|
|
|
|
button, column, container, mouse_area, pick_list, row, shader, stack, text, text_input,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
Row, Space,
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
use iced::window;
|
|
|
|
|
|
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
|
|
|
|
|
|
2026-05-27 00:08:37 +03:00
|
|
|
|
/// `pick_list` requires its items to implement `Display`; acadrust's
|
|
|
|
|
|
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
|
|
|
|
|
|
/// so wrap it locally with a friendly name renderer.
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
|
pub(super) struct RenderModeChoice(pub acadrust::entities::ViewportRenderMode);
|
|
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for RenderModeChoice {
|
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
|
use acadrust::entities::ViewportRenderMode as M;
|
|
|
|
|
|
f.write_str(match self.0 {
|
|
|
|
|
|
M::Wireframe2D => "Wireframe 2D",
|
|
|
|
|
|
M::Wireframe3D => "Wireframe 3D",
|
|
|
|
|
|
M::HiddenLine => "Hidden Line",
|
|
|
|
|
|
M::FlatShaded => "Flat Shaded",
|
|
|
|
|
|
M::GouraudShaded => "Gouraud Shaded",
|
|
|
|
|
|
M::FlatShadedWithEdges => "Flat Shaded + Edges",
|
|
|
|
|
|
M::GouraudShadedWithEdges => "Gouraud Shaded + Edges",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
|
impl OpenCADStudio {
|
2026-03-22 22:44:32 +03:00
|
|
|
|
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
|
2026-04-08 22:36:28 +03:00
|
|
|
|
// ── Floating panel windows ─────────────────────────────────────────
|
2026-03-22 22:44:32 +03:00
|
|
|
|
if Some(window_id) == self.layer_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
return tab.layers.view_window();
|
|
|
|
|
|
}
|
2026-04-08 22:36:28 +03:00
|
|
|
|
if Some(window_id) == self.page_setup_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::page_setup::view_window(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
&self.page_setup_w,
|
|
|
|
|
|
&self.page_setup_h,
|
|
|
|
|
|
&self.page_setup_plot_area,
|
|
|
|
|
|
self.page_setup_center,
|
|
|
|
|
|
&self.page_setup_offset_x,
|
|
|
|
|
|
&self.page_setup_offset_y,
|
|
|
|
|
|
&self.page_setup_rotation,
|
|
|
|
|
|
&self.page_setup_scale,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.textstyle_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
.collect();
|
2026-06-03 17:43:12 +03:00
|
|
|
|
let (backward, upside_down, annotative) = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.get(&self.textstyle_selected)
|
|
|
|
|
|
.map(|s| (s.flags.backward, s.flags.upside_down, s.annotative))
|
|
|
|
|
|
.unwrap_or((false, false, false));
|
|
|
|
|
|
return crate::ui::textstyle::view_window(crate::ui::textstyle::TextStyleView {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
styles,
|
2026-06-03 17:43:12 +03:00
|
|
|
|
selected: &self.textstyle_selected,
|
|
|
|
|
|
font_buf: &self.textstyle_font,
|
|
|
|
|
|
width_buf: &self.textstyle_width,
|
|
|
|
|
|
oblique_buf: &self.textstyle_oblique,
|
|
|
|
|
|
height_buf: &self.textstyle_height,
|
|
|
|
|
|
bigfont_buf: &self.textstyle_bigfont,
|
|
|
|
|
|
ttf_buf: &self.textstyle_ttf,
|
|
|
|
|
|
backward,
|
|
|
|
|
|
upside_down,
|
|
|
|
|
|
annotative,
|
|
|
|
|
|
});
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.tablestyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|o| {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = o {
|
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-08 22:36:28 +03:00
|
|
|
|
.collect();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| {
|
|
|
|
|
|
if let ObjectType::TableStyle(s) = o {
|
|
|
|
|
|
if s.name == self.tablestyle_selected {
|
|
|
|
|
|
Some(s)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return crate::ui::tablestyle::view_window(
|
|
|
|
|
|
styles,
|
|
|
|
|
|
&self.tablestyle_selected,
|
|
|
|
|
|
selected_style,
|
2026-06-03 20:13:30 +03:00
|
|
|
|
&self.ts_hmargin,
|
|
|
|
|
|
&self.ts_vmargin,
|
2026-06-03 21:09:37 +03:00
|
|
|
|
&self.ts_description,
|
2026-06-03 20:28:58 +03:00
|
|
|
|
&self.ts_cell_textstyle,
|
|
|
|
|
|
&self.ts_cell_height,
|
|
|
|
|
|
&self.ts_cell_textcolor,
|
|
|
|
|
|
&self.ts_cell_fillcolor,
|
2026-06-03 21:09:37 +03:00
|
|
|
|
&self.ts_cell_datatype,
|
|
|
|
|
|
&self.ts_cell_unittype,
|
|
|
|
|
|
&self.ts_cell_format,
|
|
|
|
|
|
&self.ts_border_lw,
|
|
|
|
|
|
&self.ts_border_color,
|
|
|
|
|
|
&self.ts_border_spacing,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.mlstyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|o| {
|
|
|
|
|
|
if let ObjectType::MLineStyle(s) = o {
|
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-04-08 22:36:28 +03:00
|
|
|
|
.collect();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| {
|
|
|
|
|
|
if let ObjectType::MLineStyle(s) = o {
|
|
|
|
|
|
if s.name == self.mlstyle_selected {
|
|
|
|
|
|
Some(s)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return crate::ui::mlstyle::view_window(
|
|
|
|
|
|
styles,
|
|
|
|
|
|
&self.mlstyle_selected,
|
|
|
|
|
|
selected_style,
|
|
|
|
|
|
tab.scene.document.header.multiline_style.clone(),
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
2026-06-03 20:25:14 +03:00
|
|
|
|
if Some(window_id) == self.mleaderstyle_window {
|
|
|
|
|
|
use acadrust::objects::ObjectType;
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|o| {
|
|
|
|
|
|
if let ObjectType::MultiLeaderStyle(s) = o {
|
|
|
|
|
|
Some(s.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let selected_style = tab.scene.document.objects.values().find_map(|o| {
|
|
|
|
|
|
if let ObjectType::MultiLeaderStyle(s) = o {
|
|
|
|
|
|
if s.name == self.mleaderstyle_selected {
|
|
|
|
|
|
Some(s)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
feat(styles): complete MLeader field coverage + Dimension DIMUNIT
MLeader Style editor now exposes every MultiLeaderStyle field: line
weight, line type / arrowhead block / text style / block-content blocks
(handle dropdowns from the document's tables), the four text attachments
+ attachment direction, the full block-content group (color, connection,
rotation, X/Y/Z scale, enable scale/rotation), leader & multileader draw
order, align space, and description.
Dimension Style editor adds the last uncovered DIMUNIT field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:04:22 +03:00
|
|
|
|
let doc = &tab.scene.document;
|
|
|
|
|
|
let mut block_opts: Vec<String> = vec!["None".to_string()];
|
|
|
|
|
|
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
|
|
|
|
|
|
let mut lt_opts: Vec<String> = vec!["None".to_string()];
|
|
|
|
|
|
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
|
|
|
|
|
|
let mut textstyle_opts: Vec<String> = vec!["None".to_string()];
|
|
|
|
|
|
textstyle_opts.extend(doc.text_styles.iter().map(|t| t.name.clone()));
|
|
|
|
|
|
let opt_block = |h: Option<acadrust::types::Handle>| -> String {
|
|
|
|
|
|
match h {
|
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|b| b.handle == h)
|
|
|
|
|
|
.map(|b| b.name.clone())
|
|
|
|
|
|
.unwrap_or_else(|| "None".to_string()),
|
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let opt_lt = |h: Option<acadrust::types::Handle>| -> String {
|
|
|
|
|
|
match h {
|
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
|
.line_types
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|lt| lt.handle == h)
|
|
|
|
|
|
.map(|lt| lt.name.clone())
|
|
|
|
|
|
.unwrap_or_else(|| "None".to_string()),
|
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let opt_ts = |h: Option<acadrust::types::Handle>| -> String {
|
|
|
|
|
|
match h {
|
|
|
|
|
|
Some(h) => doc
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|t| t.handle == h)
|
|
|
|
|
|
.map(|t| t.name.clone())
|
|
|
|
|
|
.unwrap_or_else(|| "None".to_string()),
|
|
|
|
|
|
None => "None".to_string(),
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let (line_type_name, arrowhead_name, text_style_name, block_content_name) =
|
|
|
|
|
|
match selected_style {
|
|
|
|
|
|
Some(s) => (
|
|
|
|
|
|
opt_lt(s.line_type_handle),
|
|
|
|
|
|
opt_block(s.arrowhead_handle),
|
|
|
|
|
|
opt_ts(s.text_style_handle),
|
|
|
|
|
|
opt_block(s.block_content_handle),
|
|
|
|
|
|
),
|
|
|
|
|
|
None => Default::default(),
|
|
|
|
|
|
};
|
2026-06-03 20:25:14 +03:00
|
|
|
|
return crate::ui::mleaderstyle::view_window(crate::ui::mleaderstyle::MLeaderStyleView {
|
|
|
|
|
|
styles,
|
|
|
|
|
|
selected: &self.mleaderstyle_selected,
|
|
|
|
|
|
style: selected_style,
|
|
|
|
|
|
current: tab.active_mleader_style.clone(),
|
|
|
|
|
|
landing_distance: &self.mls_landing_distance,
|
|
|
|
|
|
landing_gap: &self.mls_landing_gap,
|
|
|
|
|
|
arrowhead_size: &self.mls_arrowhead_size,
|
|
|
|
|
|
text_height: &self.mls_text_height,
|
|
|
|
|
|
scale_factor: &self.mls_scale_factor,
|
|
|
|
|
|
break_gap: &self.mls_break_gap,
|
|
|
|
|
|
first_seg_angle: &self.mls_first_seg_angle,
|
|
|
|
|
|
second_seg_angle: &self.mls_second_seg_angle,
|
|
|
|
|
|
max_points: &self.mls_max_points,
|
|
|
|
|
|
default_text: &self.mls_default_text,
|
|
|
|
|
|
line_color: &self.mls_line_color,
|
|
|
|
|
|
text_color: &self.mls_text_color,
|
feat(styles): complete MLeader field coverage + Dimension DIMUNIT
MLeader Style editor now exposes every MultiLeaderStyle field: line
weight, line type / arrowhead block / text style / block-content blocks
(handle dropdowns from the document's tables), the four text attachments
+ attachment direction, the full block-content group (color, connection,
rotation, X/Y/Z scale, enable scale/rotation), leader & multileader draw
order, align space, and description.
Dimension Style editor adds the last uncovered DIMUNIT field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:04:22 +03:00
|
|
|
|
description: &self.mls_description,
|
|
|
|
|
|
line_weight: &self.mls_line_weight,
|
|
|
|
|
|
align_space: &self.mls_align_space,
|
|
|
|
|
|
block_color: &self.mls_block_color,
|
|
|
|
|
|
block_rotation: &self.mls_block_rotation,
|
|
|
|
|
|
block_scale_x: &self.mls_block_scale_x,
|
|
|
|
|
|
block_scale_y: &self.mls_block_scale_y,
|
|
|
|
|
|
block_scale_z: &self.mls_block_scale_z,
|
|
|
|
|
|
block_opts,
|
|
|
|
|
|
lt_opts,
|
|
|
|
|
|
textstyle_opts,
|
|
|
|
|
|
line_type_name,
|
|
|
|
|
|
arrowhead_name,
|
|
|
|
|
|
text_style_name,
|
|
|
|
|
|
block_content_name,
|
2026-06-03 20:25:14 +03:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-08 22:36:28 +03:00
|
|
|
|
if Some(window_id) == self.layout_manager_window {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let layouts = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let current = self.tabs[i].scene.current_layout.clone();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
return crate::ui::layout_manager::view_window(
|
|
|
|
|
|
layouts,
|
|
|
|
|
|
&self.layout_manager_selected,
|
|
|
|
|
|
&self.layout_manager_rename_buf,
|
|
|
|
|
|
current,
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.plotstyle_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::plotstyle::view_window(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
self.active_plot_style.as_ref(),
|
|
|
|
|
|
self.plotstyle_panel_aci,
|
|
|
|
|
|
&self.ps_color_buf,
|
|
|
|
|
|
&self.ps_lineweight_buf,
|
|
|
|
|
|
&self.ps_screening_buf,
|
2026-04-08 22:36:28 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.dimstyle_window {
|
|
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.dim_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
.collect();
|
2026-06-03 20:34:09 +03:00
|
|
|
|
let doc = &tab.scene.document;
|
2026-06-03 20:44:37 +03:00
|
|
|
|
// Dropdown options (names must match the records exactly so the
|
|
|
|
|
|
// selection can be resolved back to a handle on the update side).
|
|
|
|
|
|
let mut block_opts: Vec<String> = vec!["Default".to_string()];
|
|
|
|
|
|
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
|
|
|
|
|
|
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
|
|
|
|
|
|
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
|
2026-06-03 20:34:09 +03:00
|
|
|
|
let blk_name = |h: acadrust::types::Handle| -> String {
|
|
|
|
|
|
if h.is_null() {
|
|
|
|
|
|
"Default".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
doc.block_records
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|b| b.handle == h)
|
2026-06-03 20:44:37 +03:00
|
|
|
|
.map(|b| b.name.clone())
|
|
|
|
|
|
.unwrap_or_else(|| "Default".to_string())
|
2026-06-03 20:34:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let lt_name = |h: acadrust::types::Handle| -> String {
|
|
|
|
|
|
if h.is_null() {
|
|
|
|
|
|
"ByBlock".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
doc.line_types
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|lt| lt.handle == h)
|
|
|
|
|
|
.map(|lt| lt.name.clone())
|
2026-06-03 20:44:37 +03:00
|
|
|
|
.unwrap_or_else(|| "ByBlock".to_string())
|
2026-06-03 20:34:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let ds_sel = doc.dim_styles.get(&self.dimstyle_selected);
|
|
|
|
|
|
let (
|
|
|
|
|
|
dimblk_name,
|
|
|
|
|
|
dimblk1_name,
|
|
|
|
|
|
dimblk2_name,
|
|
|
|
|
|
dimldrblk_name,
|
|
|
|
|
|
dimltex_name,
|
|
|
|
|
|
dimltex1_name,
|
|
|
|
|
|
dimltex2_name,
|
|
|
|
|
|
) = match ds_sel {
|
|
|
|
|
|
Some(d) => (
|
|
|
|
|
|
blk_name(d.dimblk),
|
|
|
|
|
|
blk_name(d.dimblk1),
|
|
|
|
|
|
blk_name(d.dimblk2),
|
|
|
|
|
|
blk_name(d.dimldrblk),
|
|
|
|
|
|
lt_name(d.dimltex_handle),
|
|
|
|
|
|
lt_name(d.dimltex1_handle),
|
|
|
|
|
|
lt_name(d.dimltex2_handle),
|
|
|
|
|
|
),
|
|
|
|
|
|
None => Default::default(),
|
|
|
|
|
|
};
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::dimstyle::view_window(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
styles,
|
|
|
|
|
|
&self.dimstyle_selected,
|
|
|
|
|
|
self.dimstyle_tab,
|
2026-04-08 22:56:51 +03:00
|
|
|
|
crate::ui::dimstyle::DimStyleValues {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
dimdle: &self.ds_dimdle,
|
|
|
|
|
|
dimdli: &self.ds_dimdli,
|
|
|
|
|
|
dimgap: &self.ds_dimgap,
|
|
|
|
|
|
dimexe: &self.ds_dimexe,
|
|
|
|
|
|
dimexo: &self.ds_dimexo,
|
|
|
|
|
|
dimsd1: self.ds_dimsd1,
|
|
|
|
|
|
dimsd2: self.ds_dimsd2,
|
|
|
|
|
|
dimse1: self.ds_dimse1,
|
|
|
|
|
|
dimse2: self.ds_dimse2,
|
|
|
|
|
|
dimasz: &self.ds_dimasz,
|
|
|
|
|
|
dimcen: &self.ds_dimcen,
|
|
|
|
|
|
dimtsz: &self.ds_dimtsz,
|
|
|
|
|
|
dimtxt: &self.ds_dimtxt,
|
|
|
|
|
|
dimtxsty: &self.ds_dimtxsty,
|
|
|
|
|
|
dimtad: &self.ds_dimtad,
|
|
|
|
|
|
dimtih: self.ds_dimtih,
|
|
|
|
|
|
dimtoh: self.ds_dimtoh,
|
|
|
|
|
|
dimscale: &self.ds_dimscale,
|
|
|
|
|
|
dimlfac: &self.ds_dimlfac,
|
|
|
|
|
|
dimlunit: &self.ds_dimlunit,
|
|
|
|
|
|
dimdec: &self.ds_dimdec,
|
|
|
|
|
|
dimpost: &self.ds_dimpost,
|
|
|
|
|
|
dimtol: self.ds_dimtol,
|
|
|
|
|
|
dimlim: self.ds_dimlim,
|
|
|
|
|
|
dimtp: &self.ds_dimtp,
|
|
|
|
|
|
dimtm: &self.ds_dimtm,
|
|
|
|
|
|
dimtdec: &self.ds_dimtdec,
|
|
|
|
|
|
dimtfac: &self.ds_dimtfac,
|
2026-06-03 17:49:38 +03:00
|
|
|
|
annotative: self.ds_annotative,
|
feat(dimstyle): expand Dimension Style editor to the full field set
Adds the remaining ~40 DIMSTYLE variables across the tabs (and a new
Alternate Units tab): dim/ext line colors+lineweights, fixed-length ext
lines, separate arrow blocks, arc symbol, jog angle, text color/justify/
vertical pos/fill/direction, fit options (DIMATFIT/DIMTIX/DIMSOXD/DIMTMOVE/
DIMUPT/DIMTOFL/DIMFIT), decimal separator, rounding, zero suppression,
fraction + angular units, alternate units, and tolerance vert-just/zero
suppression. Only the arrowhead-block / extension-line-linetype Handle
references are left (they need a block/linetype picker).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:09:08 +03:00
|
|
|
|
dimclrd: &self.ds_dimclrd,
|
|
|
|
|
|
dimlwd: &self.ds_dimlwd,
|
|
|
|
|
|
dimclre: &self.ds_dimclre,
|
|
|
|
|
|
dimlwe: &self.ds_dimlwe,
|
|
|
|
|
|
dimfxl: &self.ds_dimfxl,
|
|
|
|
|
|
dimfxlon: self.ds_dimfxlon,
|
|
|
|
|
|
dimsah: self.ds_dimsah,
|
|
|
|
|
|
dimarcsym: &self.ds_dimarcsym,
|
|
|
|
|
|
dimjogang: &self.ds_dimjogang,
|
|
|
|
|
|
dimclrt: &self.ds_dimclrt,
|
|
|
|
|
|
dimjust: &self.ds_dimjust,
|
|
|
|
|
|
dimtvp: &self.ds_dimtvp,
|
|
|
|
|
|
dimtfill: &self.ds_dimtfill,
|
|
|
|
|
|
dimtfillclr: &self.ds_dimtfillclr,
|
|
|
|
|
|
dimtxtdirection: self.ds_dimtxtdirection,
|
|
|
|
|
|
dimatfit: &self.ds_dimatfit,
|
|
|
|
|
|
dimtix: self.ds_dimtix,
|
|
|
|
|
|
dimsoxd: self.ds_dimsoxd,
|
|
|
|
|
|
dimtmove: &self.ds_dimtmove,
|
|
|
|
|
|
dimupt: self.ds_dimupt,
|
|
|
|
|
|
dimtofl: self.ds_dimtofl,
|
|
|
|
|
|
dimfit: &self.ds_dimfit,
|
|
|
|
|
|
dimdsep: &self.ds_dimdsep,
|
|
|
|
|
|
dimrnd: &self.ds_dimrnd,
|
|
|
|
|
|
dimzin: &self.ds_dimzin,
|
|
|
|
|
|
dimfrac: &self.ds_dimfrac,
|
|
|
|
|
|
dimaunit: &self.ds_dimaunit,
|
|
|
|
|
|
dimadec: &self.ds_dimadec,
|
feat(styles): complete MLeader field coverage + Dimension DIMUNIT
MLeader Style editor now exposes every MultiLeaderStyle field: line
weight, line type / arrowhead block / text style / block-content blocks
(handle dropdowns from the document's tables), the four text attachments
+ attachment direction, the full block-content group (color, connection,
rotation, X/Y/Z scale, enable scale/rotation), leader & multileader draw
order, align space, and description.
Dimension Style editor adds the last uncovered DIMUNIT field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:04:22 +03:00
|
|
|
|
dimunit: &self.ds_dimunit,
|
feat(dimstyle): expand Dimension Style editor to the full field set
Adds the remaining ~40 DIMSTYLE variables across the tabs (and a new
Alternate Units tab): dim/ext line colors+lineweights, fixed-length ext
lines, separate arrow blocks, arc symbol, jog angle, text color/justify/
vertical pos/fill/direction, fit options (DIMATFIT/DIMTIX/DIMSOXD/DIMTMOVE/
DIMUPT/DIMTOFL/DIMFIT), decimal separator, rounding, zero suppression,
fraction + angular units, alternate units, and tolerance vert-just/zero
suppression. Only the arrowhead-block / extension-line-linetype Handle
references are left (they need a block/linetype picker).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:09:08 +03:00
|
|
|
|
dimazin: &self.ds_dimazin,
|
|
|
|
|
|
dimalt: self.ds_dimalt,
|
|
|
|
|
|
dimaltf: &self.ds_dimaltf,
|
|
|
|
|
|
dimaltd: &self.ds_dimaltd,
|
|
|
|
|
|
dimaltu: &self.ds_dimaltu,
|
|
|
|
|
|
dimalttd: &self.ds_dimalttd,
|
|
|
|
|
|
dimaltrnd: &self.ds_dimaltrnd,
|
|
|
|
|
|
dimapost: &self.ds_dimapost,
|
|
|
|
|
|
dimaltz: &self.ds_dimaltz,
|
|
|
|
|
|
dimalttz: &self.ds_dimalttz,
|
|
|
|
|
|
dimtolj: &self.ds_dimtolj,
|
|
|
|
|
|
dimtzin: &self.ds_dimtzin,
|
2026-06-03 20:34:09 +03:00
|
|
|
|
dimblk_name,
|
|
|
|
|
|
dimblk1_name,
|
|
|
|
|
|
dimblk2_name,
|
|
|
|
|
|
dimldrblk_name,
|
|
|
|
|
|
dimltex_name,
|
|
|
|
|
|
dimltex1_name,
|
|
|
|
|
|
dimltex2_name,
|
2026-06-03 20:44:37 +03:00
|
|
|
|
block_opts,
|
|
|
|
|
|
lt_opts,
|
2026-04-08 22:56:51 +03:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
if Some(window_id) == self.shortcuts_window {
|
2026-04-08 22:56:51 +03:00
|
|
|
|
return crate::ui::shortcuts::view_window(&self.shortcut_overrides);
|
2026-04-08 22:36:28 +03:00
|
|
|
|
}
|
2026-04-22 22:22:33 +03:00
|
|
|
|
if Some(window_id) == self.about_window {
|
|
|
|
|
|
return crate::ui::about::view_window();
|
|
|
|
|
|
}
|
2026-05-16 19:55:57 +03:00
|
|
|
|
if Some(window_id) == self.update_notice_window {
|
|
|
|
|
|
let latest = self
|
|
|
|
|
|
.update_notice_version
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.unwrap_or("?");
|
2026-05-22 12:55:37 +03:00
|
|
|
|
let body = self.update_notice_body.as_deref().unwrap_or("");
|
|
|
|
|
|
return crate::ui::update_notice::view_window(latest, body);
|
2026-05-16 19:55:57 +03:00
|
|
|
|
}
|
2026-05-01 11:31:11 +03:00
|
|
|
|
if Some(window_id) == self.unsaved_dialog_window {
|
|
|
|
|
|
let tab_name = match &self.pending_close {
|
|
|
|
|
|
Some(super::PendingClose::Tab(idx)) => self
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.tabs
|
|
|
|
|
|
.get(*idx)
|
2026-05-01 11:31:11 +03:00
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
|
Some(super::PendingClose::Quit) => self
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.tabs
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|t| t.dirty)
|
2026-05-01 11:31:11 +03:00
|
|
|
|
.map(|t| t.tab_display_name())
|
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
|
None => String::new(),
|
|
|
|
|
|
};
|
|
|
|
|
|
return unsaved_changes_dialog_window(&tab_name);
|
|
|
|
|
|
}
|
2026-05-01 16:12:32 +03:00
|
|
|
|
if Some(window_id) == self.save_dialog_window {
|
|
|
|
|
|
return save_as_dialog_window(
|
|
|
|
|
|
&self.save_dialog_filename,
|
|
|
|
|
|
&self.save_dialog_folder,
|
2026-05-01 16:20:38 +03:00
|
|
|
|
&self.save_dialog_entries,
|
2026-05-01 16:12:32 +03:00
|
|
|
|
&self.save_dialog_format,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let tab = &self.tabs[i];
|
|
|
|
|
|
let is_paper = tab.scene.current_layout != "Model";
|
2026-06-02 20:33:51 +03:00
|
|
|
|
// Start tab: render welcome page in place of the viewport.
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
// Surrounding chrome (tab bar, status bar) stays; the welcome widget
|
|
|
|
|
|
// returned here also flags the rest of `view` to skip drawing-only
|
|
|
|
|
|
// overlays via `tab.is_start`.
|
2026-06-02 20:33:51 +03:00
|
|
|
|
// Unified GPU widget for both layouts. A paper layout renders through
|
|
|
|
|
|
// the same shader as model space: a full-canvas top-locked "sheet"
|
|
|
|
|
|
// viewport draws the layout's own geometry (white sheet + entities +
|
|
|
|
|
|
// borders) and the floating content viewports blit on top.
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
let viewport_3d: Element<'_, Message> = if tab.is_start {
|
|
|
|
|
|
start_page_view()
|
2026-04-23 02:18:41 +03:00
|
|
|
|
} else {
|
2026-05-27 00:08:37 +03:00
|
|
|
|
shader(ViewportPane::model(
|
|
|
|
|
|
&tab.scene,
|
|
|
|
|
|
self.show_viewcube,
|
|
|
|
|
|
tab.render_mode,
|
|
|
|
|
|
))
|
2026-04-23 02:18:41 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let selection_overlay = {
|
|
|
|
|
|
let sel = tab.scene.selection.borrow().clone();
|
|
|
|
|
|
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
|
|
|
|
|
|
|
|
|
|
|
let grips: Vec<overlay::GripMarker> =
|
|
|
|
|
|
if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() {
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
2026-05-28 09:45:19 +03:00
|
|
|
|
// Overlays project through the active tile's camera, so
|
|
|
|
|
|
// they must use the active tile's screen rectangle (with
|
|
|
|
|
|
// its canvas offset) — not the whole canvas — or they
|
|
|
|
|
|
// land in the wrong place in a tiled layout.
|
|
|
|
|
|
let bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let sel_h = tab.selected_handle;
|
2026-05-01 12:16:46 +03:00
|
|
|
|
let screen_grips = if is_paper {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
|
|
|
|
|
|
let half_h = cam.ortho_size();
|
|
|
|
|
|
let half_w = half_h * aspect;
|
|
|
|
|
|
let tx = cam.target.x;
|
|
|
|
|
|
let ty = cam.target.y;
|
|
|
|
|
|
drop(cam);
|
|
|
|
|
|
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
|
|
|
|
|
|
};
|
|
|
|
|
|
screen_grips
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.into_iter()
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
.filter(|(_, screen, _, _, _)| {
|
2026-03-22 22:44:32 +03:00
|
|
|
|
screen.x.is_finite()
|
|
|
|
|
|
&& screen.y.is_finite()
|
|
|
|
|
|
&& screen.x >= -bounds.width
|
|
|
|
|
|
&& screen.x <= bounds.width * 2.0
|
|
|
|
|
|
&& screen.y >= -bounds.height
|
|
|
|
|
|
&& screen.y <= bounds.height * 2.0
|
|
|
|
|
|
})
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
.map(|(grip_id, screen, _is_midpoint, shape, dir)| {
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let is_hot = tab
|
|
|
|
|
|
.active_grip
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
overlay::GripMarker {
|
|
|
|
|
|
pos: screen,
|
|
|
|
|
|
shape,
|
|
|
|
|
|
is_hot,
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
dir,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-03 19:25:55 +03:00
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
2026-05-28 09:45:19 +03:00
|
|
|
|
// Active tile rectangle (canvas-offset included) so grid / UCS
|
|
|
|
|
|
// icon / crosshair project through the active pane's camera at
|
|
|
|
|
|
// the correct place and scale.
|
|
|
|
|
|
let vp_bounds = tab.scene.active_model_tile_bounds(vw, vh);
|
2026-04-03 19:25:55 +03:00
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let grid = if self.show_grid {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
|
|
|
|
|
Some(overlay::GridParams {
|
2026-04-03 19:25:55 +03:00
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
|
bounds: vp_bounds,
|
2026-03-22 22:44:32 +03:00
|
|
|
|
plane,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-04-03 19:25:55 +03:00
|
|
|
|
|
|
|
|
|
|
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
|
|
|
|
|
let cam = tab.scene.camera.borrow();
|
|
|
|
|
|
Some(overlay::UcsIconParams {
|
|
|
|
|
|
view_proj: cam.view_proj(vp_bounds),
|
|
|
|
|
|
bounds: vp_bounds,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 00:26:54 +03:00
|
|
|
|
// OST tracking points → screen positions.
|
|
|
|
|
|
let ost_points: Vec<overlay::OstTrackPoint> = if self.snapper.otrack_enabled {
|
|
|
|
|
|
let vp_mat = tab.scene.camera.borrow().view_proj(vp_bounds);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
self.snapper
|
|
|
|
|
|
.tracking_points
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|&wp| {
|
|
|
|
|
|
let ndc = vp_mat.project_point3(wp);
|
|
|
|
|
|
overlay::OstTrackPoint {
|
|
|
|
|
|
screen: iced::Point::new(
|
|
|
|
|
|
(ndc.x + 1.0) * 0.5 * vp_bounds.width,
|
|
|
|
|
|
(1.0 - ndc.y) * 0.5 * vp_bounds.height,
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect()
|
2026-04-08 00:26:54 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-28 18:42:39 +03:00
|
|
|
|
// Model-space tile dividers (none in paper / single-tile layouts).
|
|
|
|
|
|
let tile_edges = if !is_paper {
|
|
|
|
|
|
tab.scene.model_tile_edges()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
overlay::selection_overlay(
|
|
|
|
|
|
sel,
|
|
|
|
|
|
snap_info,
|
|
|
|
|
|
grips,
|
|
|
|
|
|
grid,
|
|
|
|
|
|
ucs_icon,
|
|
|
|
|
|
ost_points,
|
|
|
|
|
|
tab.last_cursor_screen,
|
|
|
|
|
|
!is_paper && self.show_viewcube,
|
2026-05-28 18:42:39 +03:00
|
|
|
|
tile_edges,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
)
|
2026-03-22 22:44:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-27 00:08:37 +03:00
|
|
|
|
// Render-mode picker, top-left of the viewport. Drives whether
|
|
|
|
|
|
// 3D face / mesh fills and edges are rendered for the active
|
|
|
|
|
|
// tab. Hatch fills are deliberately *not* gated by this — the
|
|
|
|
|
|
// document's FILLMODE still owns 2D fill state.
|
2026-05-27 18:05:09 +03:00
|
|
|
|
let info = render_mode_picker(tab.render_mode);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let viewport_mouse = mouse_area(container(
|
|
|
|
|
|
iced::widget::Space::new().width(Fill).height(Fill),
|
|
|
|
|
|
))
|
|
|
|
|
|
.on_move(Message::ViewportMove)
|
|
|
|
|
|
.on_press(Message::ViewportLeftPress)
|
|
|
|
|
|
.on_release(Message::ViewportLeftRelease)
|
|
|
|
|
|
.on_right_press(Message::ViewportRightPress)
|
|
|
|
|
|
.on_right_release(Message::ViewportRightRelease)
|
|
|
|
|
|
.on_middle_press(Message::ViewportMiddlePress)
|
|
|
|
|
|
.on_middle_release(Message::ViewportMiddleRelease)
|
|
|
|
|
|
.on_scroll(Message::ViewportScroll)
|
|
|
|
|
|
.on_exit(Message::ViewportExit);
|
|
|
|
|
|
|
|
|
|
|
|
let bg_color = if is_paper {
|
2026-04-23 14:05:07 +03:00
|
|
|
|
// Desk color — matches the DESK constant in paper_canvas.rs.
|
2026-05-12 10:36:15 +03:00
|
|
|
|
Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.24,
|
|
|
|
|
|
b: 0.28,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
2026-04-06 23:26:26 +03:00
|
|
|
|
tab.bg_color
|
|
|
|
|
|
.map(|[r, g, b, a]| Color { r, g, b, a })
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.unwrap_or(Color {
|
|
|
|
|
|
r: 0.11,
|
|
|
|
|
|
g: 0.11,
|
|
|
|
|
|
b: 0.11,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
})
|
2026-03-22 22:44:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
// Dynamic input overlay — editable boxes near the cursor, one per
|
|
|
|
|
|
// quantity the active command is asking for (X/Y, or polar
|
|
|
|
|
|
// distance+angle, or a single distance/angle). TAB moves focus
|
|
|
|
|
|
// between boxes; typing locks a box to a fixed value while the
|
|
|
|
|
|
// rest keep tracking the cursor. The field set is maintained in
|
|
|
|
|
|
// `tab.dyn_fields` by `sync_dyn_fields`.
|
2026-04-08 00:12:09 +03:00
|
|
|
|
let dyn_input_overlay: Option<Element<'_, Message>> =
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
if self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty() {
|
2026-04-08 00:12:09 +03:00
|
|
|
|
let w = tab.last_cursor_world;
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
let base = self.last_point;
|
|
|
|
|
|
let boxes: Vec<overlay::DynBox> = tab
|
|
|
|
|
|
.dyn_fields
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.enumerate()
|
|
|
|
|
|
.map(|(idx, f)| overlay::DynBox {
|
|
|
|
|
|
label: dyn_component_label(f.component),
|
|
|
|
|
|
value: dyn_component_value(f, w, base),
|
|
|
|
|
|
active: idx == tab.dyn_active,
|
|
|
|
|
|
locked: f.locked(),
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, boxes))
|
2026-04-08 00:12:09 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
let mut viewport_stack = if tab.is_start {
|
|
|
|
|
|
// Start tab: only the welcome widget over a flat background.
|
|
|
|
|
|
// Skip every drawing-only overlay (selection markers, snap info,
|
|
|
|
|
|
// mouse-area capturing draw clicks, viewcube, nav toolbar, …).
|
|
|
|
|
|
stack![container(viewport_3d)
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
.height(Fill)]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
2026-05-28 10:03:01 +03:00
|
|
|
|
} else if is_paper {
|
2026-06-02 20:25:50 +03:00
|
|
|
|
// Paper layout: the GPU shader renders everything — the desk is the
|
|
|
|
|
|
// container background, the white sheet + paper entities + borders
|
|
|
|
|
|
// come from the full-canvas top-locked "sheet" viewport, and the
|
|
|
|
|
|
// floating content viewports overlay it (same path as model space).
|
|
|
|
|
|
const DESK: Color = Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 };
|
2026-05-28 10:03:01 +03:00
|
|
|
|
stack![
|
2026-06-02 20:25:50 +03:00
|
|
|
|
container(viewport_3d)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(DESK)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
2026-05-28 10:03:01 +03:00
|
|
|
|
selection_overlay,
|
|
|
|
|
|
viewport_mouse,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
stack![
|
|
|
|
|
|
container(viewport_3d)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(bg_color)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
selection_overlay,
|
|
|
|
|
|
viewport_mouse,
|
2026-05-27 18:17:10 +03:00
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Model-space render-mode picker, top-left. Sits ABOVE the
|
|
|
|
|
|
// viewport mouse_area so clicks inside its bounds reach it
|
|
|
|
|
|
// instead of the shader behind it; `opaque` stops them bubbling
|
|
|
|
|
|
// further. Outside the chip the Fill container is transparent so
|
|
|
|
|
|
// viewport drawing / selection is unaffected. In a paper layout
|
|
|
|
|
|
// the active viewport gets its own picker (below) instead.
|
|
|
|
|
|
if !is_paper && !tab.is_start {
|
2026-05-27 22:33:36 +03:00
|
|
|
|
// Render-mode picker + viewport-split buttons (horizontal /
|
|
|
|
|
|
// vertical divider of the active Model tile).
|
|
|
|
|
|
let split_btn = |glyph: &'static str, horizontal: bool| {
|
|
|
|
|
|
button(text(glyph).size(13).color(Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 0.85,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.on_press(Message::SplitModelViewport(horizontal))
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.style(|_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
iced::widget::button::Status::Hovered => Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.20,
|
|
|
|
|
|
b: 0.20,
|
|
|
|
|
|
a: 0.85,
|
|
|
|
|
|
},
|
|
|
|
|
|
_ => Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.10,
|
|
|
|
|
|
a: 0.75,
|
|
|
|
|
|
},
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.35,
|
|
|
|
|
|
g: 0.35,
|
|
|
|
|
|
b: 0.35,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 0.85,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
let bar = row![info, split_btn("▤", true), split_btn("▥", false)].spacing(4);
|
2026-05-28 09:45:19 +03:00
|
|
|
|
// Position the bar at the active model tile's top-left corner so
|
|
|
|
|
|
// it follows the active panel in a tiled layout (full canvas when
|
|
|
|
|
|
// a single tile fills the window). Leading Spaces offset it.
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
|
|
|
|
|
let bar_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(rect.y.max(0.0))),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(rect.x.max(0.0))),
|
|
|
|
|
|
iced::widget::opaque(bar),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(bar_layer);
|
2026-05-27 18:17:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Active paper-space viewport overlays: a render-mode picker in
|
|
|
|
|
|
// its top-left corner and a ViewCube hit area in its top-right,
|
|
|
|
|
|
// both layered ABOVE the viewport mouse_area so they receive
|
2026-06-02 20:33:51 +03:00
|
|
|
|
// clicks (the shader viewport sits below it). Positioned with
|
2026-05-27 18:17:10 +03:00
|
|
|
|
// leading Spaces sized to the viewport's screen rectangle.
|
|
|
|
|
|
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
|
|
|
|
|
|
tab.scene.active_viewport.and_then(|h| {
|
|
|
|
|
|
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
tab.scene.viewport_screen_rect(h, (cw, ch))
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(rect) = active_vp_rect {
|
|
|
|
|
|
let x = rect.x.max(0.0);
|
|
|
|
|
|
let y = rect.y.max(0.0);
|
2026-05-28 10:03:01 +03:00
|
|
|
|
// Highlight the active viewport with a 2-px border so its
|
|
|
|
|
|
// boundary is always visible over the GPU shader.
|
|
|
|
|
|
const VP_BORDER: Color = Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.52,
|
|
|
|
|
|
b: 0.95,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
let border_frame = container(
|
|
|
|
|
|
Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(rect.width.max(1.0)))
|
|
|
|
|
|
.height(iced::Length::Fixed(rect.height.max(1.0))),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
border: iced::Border {
|
|
|
|
|
|
color: VP_BORDER,
|
|
|
|
|
|
width: 2.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
let border_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y)),
|
|
|
|
|
|
row![Space::new().width(iced::Length::Fixed(x)), border_frame,],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(border_layer);
|
|
|
|
|
|
|
2026-05-27 18:17:10 +03:00
|
|
|
|
let vp_mode = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.active_viewport_render_mode()
|
|
|
|
|
|
.unwrap_or(acadrust::entities::ViewportRenderMode::Wireframe2D);
|
|
|
|
|
|
let picker_layer = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(y + 4.0)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(x + 4.0)),
|
|
|
|
|
|
iced::widget::opaque(render_mode_picker(vp_mode)),
|
|
|
|
|
|
],
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
2026-05-27 18:17:10 +03:00
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(picker_layer);
|
|
|
|
|
|
|
|
|
|
|
|
if self.show_viewcube {
|
|
|
|
|
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
let cube_click = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x)),
|
|
|
|
|
|
mouse_area(
|
|
|
|
|
|
iced::widget::Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
|
|
|
|
|
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_move(Message::CursorMoved)
|
|
|
|
|
|
.on_press(Message::ViewportClick),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(cube_click);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-24 01:23:06 +03:00
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
if self.show_viewcube && !is_paper && !tab.is_start {
|
2026-05-28 09:45:19 +03:00
|
|
|
|
// Place the ViewCube hit area in the active model tile's top-right
|
|
|
|
|
|
// corner so it tracks the active panel in a tiled layout. The hit
|
|
|
|
|
|
// test in update.rs already maps clicks through the active tile.
|
|
|
|
|
|
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
|
|
|
|
|
let rect = tab.scene.active_model_tile_bounds(vw, vh);
|
|
|
|
|
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
|
|
|
|
|
let cube_click = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(cube_y)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(cube_x)),
|
|
|
|
|
|
mouse_area(
|
|
|
|
|
|
iced::widget::Space::new()
|
|
|
|
|
|
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
|
|
|
|
|
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_move(Message::CursorMoved)
|
|
|
|
|
|
.on_press(Message::ViewportClick),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
2026-04-24 01:23:06 +03:00
|
|
|
|
.width(Fill)
|
2026-05-28 09:45:19 +03:00
|
|
|
|
.height(Fill);
|
2026-04-24 01:23:06 +03:00
|
|
|
|
viewport_stack = viewport_stack.push(cube_click);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 00:12:09 +03:00
|
|
|
|
if let Some(dyn_ol) = dyn_input_overlay {
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
if !tab.is_start {
|
|
|
|
|
|
viewport_stack = viewport_stack.push(dyn_ol);
|
|
|
|
|
|
}
|
2026-04-08 00:12:09 +03:00
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
2026-05-29 09:51:12 +03:00
|
|
|
|
// Multi-functional grip popup (Phase 2). One bordered container
|
|
|
|
|
|
// wraps a column of borderless item buttons so the popup reads
|
|
|
|
|
|
// as a single widget instead of stacked tiles.
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
if let Some(popup) = self.grip_popup.as_ref() {
|
|
|
|
|
|
if !tab.is_start {
|
2026-05-29 10:00:30 +03:00
|
|
|
|
// Size the row to the widest label so the selection
|
|
|
|
|
|
// highlight fills the whole row instead of just the
|
|
|
|
|
|
// text glyphs. ~7 px per character at size 12 + the
|
|
|
|
|
|
// horizontal padding (10 + 10).
|
|
|
|
|
|
let max_len = popup
|
|
|
|
|
|
.items
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|i| i.label.chars().count())
|
|
|
|
|
|
.max()
|
|
|
|
|
|
.unwrap_or(8) as f32;
|
|
|
|
|
|
let row_w = max_len * 7.0 + 24.0;
|
|
|
|
|
|
let mut col = column![]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.width(iced::Length::Fixed(row_w));
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
for (idx, item) in popup.items.iter().enumerate() {
|
|
|
|
|
|
let is_sel = idx == popup.selected;
|
|
|
|
|
|
let label = item.label;
|
|
|
|
|
|
let btn = button(text(label).size(12).color(Color::WHITE))
|
|
|
|
|
|
.on_press(Message::GripMenuPick(idx))
|
2026-05-29 09:51:12 +03:00
|
|
|
|
.padding([3, 10])
|
|
|
|
|
|
.width(Fill)
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match (is_sel, status) {
|
|
|
|
|
|
(true, _) => Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.45,
|
|
|
|
|
|
b: 0.95,
|
2026-05-29 09:51:12 +03:00
|
|
|
|
a: 1.0,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
},
|
|
|
|
|
|
(_, iced::widget::button::Status::Hovered) => Color {
|
2026-05-29 09:51:12 +03:00
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
},
|
2026-05-29 09:51:12 +03:00
|
|
|
|
_ => Color::TRANSPARENT,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
2026-05-29 09:51:12 +03:00
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
2026-05-29 09:51:12 +03:00
|
|
|
|
col = col.push(btn);
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
}
|
2026-05-29 09:51:12 +03:00
|
|
|
|
let menu_panel = container(col)
|
|
|
|
|
|
.padding(2)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.10,
|
|
|
|
|
|
a: 0.95,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.40,
|
|
|
|
|
|
g: 0.40,
|
|
|
|
|
|
b: 0.40,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
2026-05-29 15:58:23 +03:00
|
|
|
|
// Offset the menu by 12 px so the cursor doesn't land on
|
|
|
|
|
|
// the first item immediately, matching the right-click
|
|
|
|
|
|
// context menu's "panel below the click point" feel.
|
|
|
|
|
|
let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0);
|
|
|
|
|
|
viewport_stack = viewport_stack.push(position_canvas_overlay(anchor, menu_panel.into()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 23:31:36 +03:00
|
|
|
|
// Quick Properties: compact floating property panel on selection,
|
|
|
|
|
|
// anchored at the canvas top-left so it doesn't track the cursor.
|
|
|
|
|
|
if self.quick_properties && !tab.is_start {
|
|
|
|
|
|
if let Some(panel) = tab.properties.quick_view() {
|
|
|
|
|
|
viewport_stack = viewport_stack
|
|
|
|
|
|
.push(position_canvas_overlay(iced::Point::new(12.0, 12.0), panel));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
|
// Right-click context menu. Lives inside the viewport stack so
|
|
|
|
|
|
// the cursor position (canvas-relative) anchors the menu under
|
|
|
|
|
|
// the cursor instead of drifting into window-relative space.
|
|
|
|
|
|
if !tab.is_start {
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
let (ctx_pos, draworder_open) = {
|
|
|
|
|
|
let sel = tab.scene.selection.borrow();
|
|
|
|
|
|
(sel.context_menu, sel.draworder_submenu)
|
|
|
|
|
|
};
|
2026-05-29 15:58:23 +03:00
|
|
|
|
if let Some(p) = ctx_pos {
|
|
|
|
|
|
let has_cmd = tab.active_cmd.is_some();
|
|
|
|
|
|
let has_selection = !tab.scene.selected.is_empty();
|
2026-06-04 23:17:21 +03:00
|
|
|
|
let isolation_active = tab.scene.is_isolation_active();
|
2026-05-29 15:58:23 +03:00
|
|
|
|
let last_cmds: Vec<String> = self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.cmd_recall
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.rev()
|
|
|
|
|
|
.take(3)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect();
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
viewport_stack = viewport_stack.push(viewport_context_menu_overlay(
|
|
|
|
|
|
p,
|
|
|
|
|
|
has_cmd,
|
|
|
|
|
|
has_selection,
|
2026-06-04 23:17:21 +03:00
|
|
|
|
isolation_active,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
last_cmds,
|
|
|
|
|
|
draworder_open,
|
|
|
|
|
|
));
|
feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected
entity's grip the overlay opens a popup menu with entity-specific
options (Stretch / Add Vertex / Remove Vertex / Convert to
Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the
default `Stretch` entry is a no-op that lets the user click the
grip to start the normal drag.
- `GripMenuItem` + `GripMenuAction` (full vocabulary covering
stretch, add/remove vertex, convert to arc/line, reverse arrows,
text-position variants, hatch parameters, tangent direction).
- `Grippable` trait gains `grip_menu` + `apply_grip_menu` with
trait-default `Stretch`-only impls so every existing entity gets
the popup for free.
- `EntityTypeOps` dispatches both methods across the same entity
list as the existing grip / property machinery.
- `OpenCADStudio` carries `grip_hover` (dwell tracker) +
`grip_popup` (open menu). `update_grip_hover` runs from
`ViewportMove`: hit-tests grips, refreshes the dwell timer, and
opens the popup after 600 ms. Cursor drift past 80 px or a grip
click dismisses it.
- `view.rs` paints the popup as an absolute-positioned column of
buttons at the grip's screen anchor; each button publishes
`Message::GripMenuPick(idx)`.
- `LwPolyline` overrides `Grippable` manually (skips the standard
macro) so its vertex grip exposes Add/Remove Vertex and its
segment-midpoint grip exposes Add Vertex + Convert to Arc/Line.
`apply_grip_menu` performs the edits: vertex insertion at the
chord midpoint inheriting the previous bulge, vertex removal,
and bulge toggle for the arc/line conversion.
2026-05-29 09:30:54 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
// In-place MText editor (toolbar + text area), anchored at the
|
|
|
|
|
|
// insertion-point click.
|
|
|
|
|
|
if !tab.is_start {
|
2026-06-03 10:59:59 +03:00
|
|
|
|
let canvas = tab.scene.selection.borrow().vp_size;
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
if let Some(ed) = &self.mtext_editor {
|
|
|
|
|
|
let styles: Vec<String> = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
.collect();
|
2026-06-03 10:59:59 +03:00
|
|
|
|
viewport_stack = viewport_stack.push(mtext_editor_overlay(ed, styles, canvas));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
}
|
2026-05-31 14:44:18 +03:00
|
|
|
|
if let Some(ed) = &self.text_inline {
|
2026-06-03 10:59:59 +03:00
|
|
|
|
viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas));
|
2026-05-31 14:44:18 +03:00
|
|
|
|
}
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
// Properties / layers panels carry no useful state on the Start tab.
|
|
|
|
|
|
// Replace the properties panel with a Recent Documents list there.
|
|
|
|
|
|
let properties_el: Element<'_, Message> = if tab.is_start {
|
|
|
|
|
|
recent_files_panel(&self.app_menu.recent)
|
2026-06-04 19:14:04 +03:00
|
|
|
|
} else if self.show_properties && !self.clean_screen {
|
2026-04-24 01:23:06 +03:00
|
|
|
|
tab.properties.view()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Space::new().into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 19:23:19 +03:00
|
|
|
|
// Command-line sits as a bottom-centre overlay on top of the
|
|
|
|
|
|
// viewport stack rather than as a separate row in the main
|
|
|
|
|
|
// column — frees up vertical space when no command is active
|
|
|
|
|
|
// and keeps the input close to where the cursor is drawing.
|
2026-05-26 22:45:54 +03:00
|
|
|
|
// Autocomplete shows only when no command is collecting its
|
|
|
|
|
|
// own input (otherwise typed prefixes are coordinates / values).
|
|
|
|
|
|
let allow_autocomplete = tab.active_cmd.is_none();
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
// Dynamic input captures keystrokes when its fields are showing,
|
|
|
|
|
|
// so the command-line field must release focus / its on_input.
|
2026-05-31 10:16:30 +03:00
|
|
|
|
// The MText preview also captures keystrokes (typing edits it), so the
|
|
|
|
|
|
// command line must likewise release its on_input there.
|
|
|
|
|
|
let dyn_capturing = (self.dyn_input
|
|
|
|
|
|
&& tab.active_cmd.is_some()
|
|
|
|
|
|
&& !tab.dyn_fields.is_empty())
|
2026-05-31 14:44:18 +03:00
|
|
|
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|
|
|
|
|
|| self.text_inline.is_some();
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
let command_line_overlay =
|
|
|
|
|
|
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
|
2026-05-26 19:23:19 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.align_x(iced::alignment::Horizontal::Center)
|
|
|
|
|
|
.align_y(iced::alignment::Vertical::Bottom)
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
top: 0.0,
|
|
|
|
|
|
right: 0.0,
|
|
|
|
|
|
bottom: 2.0,
|
|
|
|
|
|
left: 0.0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let center_stack = iced::widget::stack![
|
|
|
|
|
|
row![properties_el, viewport_stack].width(Fill).height(Fill),
|
|
|
|
|
|
command_line_overlay,
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
2026-04-24 01:23:06 +03:00
|
|
|
|
let main_ui = container({
|
2026-06-04 19:14:04 +03:00
|
|
|
|
// Clean-screen mode drops the ribbon for a full-canvas view; the
|
|
|
|
|
|
// status bar stays so the mode can be toggled back off.
|
|
|
|
|
|
let mut col = column![];
|
|
|
|
|
|
if !self.clean_screen {
|
|
|
|
|
|
col = col.push(self.ribbon.view(
|
|
|
|
|
|
is_paper,
|
|
|
|
|
|
self.tabs[self.active_tab].history.undo_stack.len(),
|
|
|
|
|
|
self.tabs[self.active_tab].history.redo_stack.len(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-04-24 01:23:06 +03:00
|
|
|
|
if self.show_file_tabs {
|
|
|
|
|
|
col = col.push(doc_tab_bar(&self.tabs, self.active_tab));
|
|
|
|
|
|
}
|
|
|
|
|
|
col.push(center_stack)
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.push({
|
2026-05-06 17:50:46 +03:00
|
|
|
|
let is_model = tab.scene.current_layout == "Model";
|
|
|
|
|
|
let scale_pill_enabled = is_model
|
|
|
|
|
|
|| tab.scene.active_viewport.is_some()
|
|
|
|
|
|
|| tab.scene.has_selected_viewport();
|
2026-06-04 19:14:04 +03:00
|
|
|
|
// The cursor is tracked in local render space; re-add the
|
|
|
|
|
|
// model-space world offset so the readout shows true
|
|
|
|
|
|
// drawing coordinates (paper space carries no offset).
|
|
|
|
|
|
let cursor_coord = {
|
|
|
|
|
|
let lc = tab.last_cursor_world;
|
|
|
|
|
|
if is_model {
|
|
|
|
|
|
let wo = tab.scene.world_offset;
|
|
|
|
|
|
glam::Vec3::new(
|
|
|
|
|
|
lc.x + wo[0] as f32,
|
|
|
|
|
|
lc.y + wo[1] as f32,
|
|
|
|
|
|
lc.z + wo[2] as f32,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lc
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-05-06 17:50:46 +03:00
|
|
|
|
self.status_bar.view(
|
|
|
|
|
|
&self.snapper,
|
|
|
|
|
|
self.snap_popup_open,
|
|
|
|
|
|
self.ortho_mode,
|
|
|
|
|
|
self.polar_mode,
|
|
|
|
|
|
self.polar_increment_deg,
|
|
|
|
|
|
self.show_grid,
|
|
|
|
|
|
self.dyn_input,
|
|
|
|
|
|
self.snapper.otrack_enabled,
|
|
|
|
|
|
tab.scene.layout_names(),
|
|
|
|
|
|
tab.scene.current_layout.clone(),
|
|
|
|
|
|
self.layout_rename_state.as_ref(),
|
|
|
|
|
|
tab.scene.first_viewport_scale(),
|
|
|
|
|
|
tab.scene.viewport_count(),
|
|
|
|
|
|
tab.scene.active_viewport.is_some(),
|
|
|
|
|
|
self.show_layout_tabs,
|
|
|
|
|
|
tab.scene.annotation_scale,
|
|
|
|
|
|
self.scale_popup_open,
|
|
|
|
|
|
scale_pill_enabled,
|
2026-05-23 07:15:26 +03:00
|
|
|
|
tab.scene.document.header.lineweight_display,
|
2026-06-04 19:14:04 +03:00
|
|
|
|
cursor_coord,
|
|
|
|
|
|
self.clean_screen,
|
2026-06-04 22:06:42 +03:00
|
|
|
|
tab.scene.document.header.insertion_units,
|
|
|
|
|
|
self.units_popup_open,
|
2026-06-04 22:51:23 +03:00
|
|
|
|
tab.scene.is_isolation_active(),
|
2026-06-04 23:25:09 +03:00
|
|
|
|
tab.scene.transparency_display,
|
2026-06-04 23:31:36 +03:00
|
|
|
|
self.quick_properties,
|
2026-06-04 19:14:04 +03:00
|
|
|
|
&self.statusbar_config,
|
2026-05-06 17:50:46 +03:00
|
|
|
|
)
|
2026-05-12 10:36:15 +03:00
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
2026-04-24 01:23:06 +03:00
|
|
|
|
})
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.style(|_: &Theme| container::Style {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
background: Some(Background::Color(Color {
|
|
|
|
|
|
r: 0.11,
|
|
|
|
|
|
g: 0.11,
|
|
|
|
|
|
b: 0.11,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
})),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
let snap_layer: Element<'_, Message> = if self.snap_popup_open {
|
|
|
|
|
|
crate::ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-06 17:50:46 +03:00
|
|
|
|
let scale_layer: Element<'_, Message> = if self.scale_popup_open {
|
|
|
|
|
|
let is_model = tab.scene.current_layout == "Model";
|
|
|
|
|
|
crate::ui::scale_popup::scale_popup_overlay(
|
|
|
|
|
|
is_model,
|
|
|
|
|
|
tab.scene.annotation_scale,
|
|
|
|
|
|
tab.scene.first_viewport_scale(),
|
2026-06-04 16:18:01 +03:00
|
|
|
|
tab.scene.scale_list(),
|
2026-05-06 17:50:46 +03:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-04 19:14:04 +03:00
|
|
|
|
let statusbar_menu_layer: Element<'_, Message> = if self.statusbar_menu_open {
|
|
|
|
|
|
crate::ui::statusbar_menu::statusbar_menu_overlay(&self.statusbar_config)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-04 22:06:42 +03:00
|
|
|
|
let units_layer: Element<'_, Message> = if self.units_popup_open {
|
|
|
|
|
|
crate::ui::units_popup::units_popup_overlay(
|
|
|
|
|
|
tab.scene.document.header.insertion_units,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-04 23:17:21 +03:00
|
|
|
|
let isolate_layer: Element<'_, Message> = if self.isolate_popup_open {
|
|
|
|
|
|
crate::ui::isolate_popup::isolate_popup_overlay(
|
|
|
|
|
|
!tab.scene.selected.is_empty(),
|
|
|
|
|
|
tab.scene.is_isolation_active(),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let dropdown_layer: Element<'_, Message> = self
|
|
|
|
|
|
.ribbon
|
|
|
|
|
|
.dropdown_overlay(
|
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack),
|
|
|
|
|
|
&history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack),
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let layout_ctx_layer: Element<'_, Message> = if let Some(name) = &self.layout_context_menu {
|
|
|
|
|
|
layout_context_menu_overlay(name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
2026-05-29 20:30:56 +03:00
|
|
|
|
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
|
|
|
|
|
let types = tab.scene.entity_type_names_in_layout();
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
let properties = tab
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.qselect_properties(state.type_filter.as_deref());
|
|
|
|
|
|
qselect_overlay(state, &types, &properties)
|
2026-05-29 20:30:56 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-13 09:59:07 +03:00
|
|
|
|
let open_progress_layer: Element<'_, Message> = if let Some(p) = &self.opening {
|
|
|
|
|
|
crate::ui::open_progress::view(p, iced::time::Instant::now())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::widget::Space::new().width(0).height(0).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
stack![
|
|
|
|
|
|
main_ui,
|
|
|
|
|
|
self.app_menu.view(),
|
|
|
|
|
|
snap_layer,
|
|
|
|
|
|
scale_layer,
|
2026-06-04 19:14:04 +03:00
|
|
|
|
statusbar_menu_layer,
|
2026-06-04 22:06:42 +03:00
|
|
|
|
units_layer,
|
2026-06-04 23:17:21 +03:00
|
|
|
|
isolate_layer,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
dropdown_layer,
|
|
|
|
|
|
layout_ctx_layer,
|
2026-05-29 20:30:56 +03:00
|
|
|
|
qselect_layer,
|
2026-05-13 09:59:07 +03:00
|
|
|
|
open_progress_layer,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
]
|
|
|
|
|
|
.into()
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn subscription(&self) -> Subscription<Message> {
|
|
|
|
|
|
use iced::event;
|
2026-05-19 13:42:03 +03:00
|
|
|
|
// Only request per-frame ticks while something on screen is animating
|
|
|
|
|
|
// (currently just the open-progress indicator). Without this gate the
|
|
|
|
|
|
// app burned 2-3% CPU continuously redrawing an unchanged view.
|
|
|
|
|
|
// See #18.
|
|
|
|
|
|
let needs_frames = self.opening.is_some();
|
|
|
|
|
|
let frames = if needs_frames {
|
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
2026-05-26 19:28:09 +03:00
|
|
|
|
// While the command-line overlay is still displaying any
|
|
|
|
|
|
// recently-pushed history entry, re-render every frame so the
|
|
|
|
|
|
// entry disappears at the moment its visible window expires.
|
|
|
|
|
|
// The subscription auto-stops once no entry is fresh enough
|
|
|
|
|
|
// (typically within a few seconds of the last command).
|
|
|
|
|
|
let history_tick = if self.command_line.has_visible_history() {
|
|
|
|
|
|
window::frames().map(Message::Tick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
2026-05-29 09:38:37 +03:00
|
|
|
|
// While the cursor sits over a grip, request animation frames
|
|
|
|
|
|
// so the multi-functional popup opens even when the user keeps
|
|
|
|
|
|
// the mouse perfectly still — `ViewportMove` alone would never
|
|
|
|
|
|
// fire again. Auto-stops once the hover clears or the popup is
|
|
|
|
|
|
// already open.
|
|
|
|
|
|
let grip_dwell = if self.grip_hover.is_some() && self.grip_popup.is_none() {
|
|
|
|
|
|
window::frames().map(|_| Message::GripDwellTick)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
2026-05-31 13:14:35 +03:00
|
|
|
|
// Blink the MText preview caret while the editor is open.
|
|
|
|
|
|
let caret_blink = if self.mtext_editor.is_some() {
|
|
|
|
|
|
iced::time::every(std::time::Duration::from_millis(530))
|
|
|
|
|
|
.map(|_| Message::MTextCaretBlink)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Subscription::none()
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
iced::Subscription::batch([
|
2026-05-19 13:42:03 +03:00
|
|
|
|
frames,
|
2026-05-26 19:28:09 +03:00
|
|
|
|
history_tick,
|
2026-05-29 09:38:37 +03:00
|
|
|
|
grip_dwell,
|
2026-05-31 13:14:35 +03:00
|
|
|
|
caret_blink,
|
2026-03-22 22:44:32 +03:00
|
|
|
|
event::listen_with(|ev, status, win_id| {
|
|
|
|
|
|
use iced::event::Status;
|
|
|
|
|
|
match ev {
|
2026-05-01 11:31:11 +03:00
|
|
|
|
iced::Event::Window(window::Event::CloseRequested) => {
|
|
|
|
|
|
Some(Message::WindowCloseRequested(win_id))
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
iced::Event::Window(window::Event::Closed) => {
|
|
|
|
|
|
Some(Message::OsWindowClosed(win_id))
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::Event::Window(window::Event::Resized(sz)) => {
|
|
|
|
|
|
Some(Message::WindowResized(sz.width as f32, sz.height as f32))
|
|
|
|
|
|
}
|
2026-05-30 21:54:35 +03:00
|
|
|
|
iced::Event::Keyboard(keyboard::Event::ModifiersChanged(m)) => {
|
|
|
|
|
|
Some(Message::SetShiftDown(m.shift()))
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
key, modifiers, text, ..
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}) => {
|
|
|
|
|
|
let ctrl = modifiers.control();
|
|
|
|
|
|
let shift = modifiers.shift();
|
2026-05-27 17:36:06 +03:00
|
|
|
|
// Any key that produces a printable glyph types it,
|
|
|
|
|
|
// even when its logical key resolves to navigation
|
|
|
|
|
|
// (NumLock-on Numpad8 / Numpad2 arrive as
|
|
|
|
|
|
// ArrowUp / ArrowDown but still carry text "8" /
|
|
|
|
|
|
// "2"). Checked before the Arrow / history arms so
|
|
|
|
|
|
// those numpad digits aren't swallowed as history
|
|
|
|
|
|
// navigation. Whitespace / control text (Space,
|
|
|
|
|
|
// Enter, Tab) falls through to the named handlers.
|
|
|
|
|
|
if !ctrl && status == Status::Ignored {
|
|
|
|
|
|
if let Some(t) = text.as_deref() {
|
|
|
|
|
|
if !t.is_empty()
|
|
|
|
|
|
&& t.chars().all(|c| !c.is_control() && !c.is_whitespace())
|
|
|
|
|
|
{
|
|
|
|
|
|
return Some(Message::CommandAppendChar(t.to_string()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
match key {
|
2026-05-31 13:14:35 +03:00
|
|
|
|
// Space is a literal space inside the MText preview
|
|
|
|
|
|
// but finalises a command otherwise; the handler
|
|
|
|
|
|
// decides based on editor state.
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Space)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandSpace)
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Enter)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandFinalize)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Escape) => {
|
|
|
|
|
|
Some(Message::CommandEscape)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Delete)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::DeleteSelected)
|
|
|
|
|
|
}
|
2026-05-26 18:59:51 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Backspace)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::CommandBackspace)
|
|
|
|
|
|
}
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::Tab)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::DynTabNext)
|
|
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
2026-04-07 00:04:12 +03:00
|
|
|
|
Some(Message::CommandHistoryPrev)
|
|
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
2026-04-07 00:04:12 +03:00
|
|
|
|
Some(Message::CommandHistoryNext)
|
|
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
|
// Caret movement in the MText preview (no-op
|
|
|
|
|
|
// otherwise; these arrows are unused elsewhere).
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::MTextCaretMove(-1))
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight)
|
|
|
|
|
|
if status == Status::Ignored =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Some(Message::MTextCaretMove(1))
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F3) => {
|
|
|
|
|
|
Some(Message::ToggleSnapEnabled)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F7) => {
|
|
|
|
|
|
Some(Message::ToggleGrid)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F8) => {
|
|
|
|
|
|
Some(Message::ToggleOrtho)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F9) => {
|
|
|
|
|
|
Some(Message::ToggleGridSnap)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F10) => {
|
|
|
|
|
|
Some(Message::TogglePolar)
|
|
|
|
|
|
}
|
2026-04-08 00:26:54 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F11) => {
|
|
|
|
|
|
Some(Message::ToggleOTrack)
|
|
|
|
|
|
}
|
2026-04-08 00:12:09 +03:00
|
|
|
|
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
|
|
|
|
|
Some(Message::ToggleDynInput)
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
keyboard::Key::Character(c) if ctrl => match c.as_str() {
|
2026-05-03 19:55:38 +03:00
|
|
|
|
"n" => Some(Message::TabNew),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"o" => Some(Message::OpenFile),
|
|
|
|
|
|
"s" if !shift => Some(Message::SaveFile),
|
|
|
|
|
|
"s" if shift => Some(Message::SaveAs),
|
|
|
|
|
|
"z" if !shift => Some(Message::Undo),
|
|
|
|
|
|
"z" if shift => Some(Message::Redo),
|
|
|
|
|
|
"y" => Some(Message::Redo),
|
|
|
|
|
|
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
|
|
|
|
|
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
|
|
|
|
|
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
},
|
2026-05-27 17:36:06 +03:00
|
|
|
|
// Printable glyphs are already handled by the
|
|
|
|
|
|
// text guard above the match; anything reaching
|
|
|
|
|
|
// here is a non-typing key we don't bind.
|
2026-03-22 22:44:32 +03:00
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}),
|
|
|
|
|
|
])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn focus_cmd_input(&self) -> Task<Message> {
|
|
|
|
|
|
iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-23 02:18:41 +03:00
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
const BAR_BG: Color = Color {
|
|
|
|
|
|
r: 0.13,
|
|
|
|
|
|
g: 0.13,
|
|
|
|
|
|
b: 0.13,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TAB_ACTIVE: Color = Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TAB_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.18,
|
|
|
|
|
|
b: 0.18,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TAB_INACTIVE: Color = Color {
|
|
|
|
|
|
r: 0.13,
|
|
|
|
|
|
g: 0.13,
|
|
|
|
|
|
b: 0.13,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const ACCENT: Color = Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.90,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
const TEXT_ACTIVE: Color = Color::WHITE;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
const TEXT_INACTIVE: Color = Color {
|
|
|
|
|
|
r: 0.60,
|
|
|
|
|
|
g: 0.60,
|
|
|
|
|
|
b: 0.60,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const CLOSE_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.70,
|
|
|
|
|
|
g: 0.22,
|
|
|
|
|
|
b: 0.22,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BORDER_COLOR: Color = Color {
|
|
|
|
|
|
r: 0.25,
|
|
|
|
|
|
g: 0.25,
|
|
|
|
|
|
b: 0.25,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let mut bar = Row::new().spacing(0).align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
for (idx, tab) in tabs.iter().enumerate() {
|
|
|
|
|
|
let is_active = idx == active_tab;
|
|
|
|
|
|
let name = tab.tab_display_name();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let label = if tab.dirty {
|
|
|
|
|
|
format!("● {}", name)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
name
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let title_btn = button(text(label).size(12))
|
|
|
|
|
|
.on_press(Message::TabSwitch(idx))
|
|
|
|
|
|
.padding([5, 12])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match (is_active, status) {
|
|
|
|
|
|
(true, _) => TAB_ACTIVE,
|
|
|
|
|
|
(false, button::Status::Hovered) => TAB_HOVER,
|
|
|
|
|
|
_ => TAB_INACTIVE,
|
|
|
|
|
|
})),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
text_color: if is_active {
|
|
|
|
|
|
TEXT_ACTIVE
|
|
|
|
|
|
} else {
|
|
|
|
|
|
TEXT_INACTIVE
|
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
|
border: Border {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
color: if is_active {
|
|
|
|
|
|
ACCENT
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color::TRANSPARENT
|
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
// Start tab is fixed — no close button. Every other tab gets a close.
|
|
|
|
|
|
let row_inner: Row<'_, Message> = if tab.is_start {
|
|
|
|
|
|
row![title_btn].spacing(0).align_y(iced::Center)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let close_btn = button(text("×").size(11).color(Color {
|
|
|
|
|
|
r: 0.55,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.55,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.on_press(Message::TabClose(idx))
|
|
|
|
|
|
.padding([3, 5])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => CLOSE_HOVER,
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
if is_active {
|
|
|
|
|
|
TAB_ACTIVE
|
|
|
|
|
|
} else {
|
|
|
|
|
|
TAB_INACTIVE
|
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
|
}
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
},
|
2026-03-22 22:44:32 +03:00
|
|
|
|
..Default::default()
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
});
|
|
|
|
|
|
row![title_btn, close_btn].spacing(0).align_y(iced::Center)
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
bar = bar.push(
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
container(row_inner).style(move |_: &Theme| container::Style {
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT },
|
|
|
|
|
|
width: if is_active { 1.0 } else { 0.0 },
|
|
|
|
|
|
radius: 0.0.into(),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
},
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
}),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let new_btn = button(text("+").size(14).color(Color {
|
|
|
|
|
|
r: 0.65,
|
|
|
|
|
|
g: 0.65,
|
|
|
|
|
|
b: 0.65,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}))
|
|
|
|
|
|
.on_press(Message::TabNew)
|
|
|
|
|
|
.padding([4, 10])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => TAB_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
radius: 0.0.into(),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
..Default::default()
|
2026-05-12 10:36:15 +03:00
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
bar = bar.push(new_btn);
|
|
|
|
|
|
bar = bar.push(iced::widget::Space::new().width(Fill));
|
|
|
|
|
|
|
|
|
|
|
|
container(bar)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BAR_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER_COLOR,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.height(30)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.padding([0, 2])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Layout context-menu overlay ────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
|
// ── Canvas-relative overlay positioning ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Wraps `panel` in a column+row of `Space` widgets so it sits at
|
|
|
|
|
|
/// canvas-relative coordinates `(anchor.x, anchor.y)`. `panel` is wrapped
|
|
|
|
|
|
/// in `iced::widget::opaque` so mouse events on the panel itself do not
|
|
|
|
|
|
/// fall through to the viewport mouse area underneath; outside-click
|
|
|
|
|
|
/// dismissal is the caller's responsibility (handled via `ViewportLeftPress`
|
|
|
|
|
|
/// in `update.rs`, identical to how the multi-functional grip popup is
|
|
|
|
|
|
/// dismissed). Pushed into `viewport_stack` so the anchor is interpreted
|
|
|
|
|
|
/// in canvas-relative space, not window-relative.
|
|
|
|
|
|
fn position_canvas_overlay<'a>(
|
|
|
|
|
|
anchor: iced::Point,
|
|
|
|
|
|
panel: Element<'a, Message>,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
let ax = anchor.x.max(0.0);
|
|
|
|
|
|
let ay = anchor.y.max(0.0);
|
|
|
|
|
|
column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(ay)),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(iced::Length::Fixed(ax)),
|
|
|
|
|
|
iced::widget::opaque(panel),
|
|
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
// ── In-place MText editor overlay ───────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-31 10:16:30 +03:00
|
|
|
|
/// Widget id for the MText editor's text area (focused when Edit mode opens).
|
|
|
|
|
|
pub(super) const MTEXT_TEXT_ID: &str = "mtext_editor_text";
|
|
|
|
|
|
|
2026-05-31 14:44:18 +03:00
|
|
|
|
/// Widget id for the in-place TEXT editor's input (focused when it opens).
|
|
|
|
|
|
pub(super) const TEXT_INLINE_ID: &str = "text_inline_input";
|
|
|
|
|
|
|
|
|
|
|
|
/// In-place single-line TEXT editor: a plain text-entry box (no formatting
|
|
|
|
|
|
/// toolbar), anchored at the insertion-point click. Enter commits; Esc cancels.
|
2026-06-03 10:59:59 +03:00
|
|
|
|
fn text_inline_overlay(
|
|
|
|
|
|
ed: &super::text_inline::TextInlineState,
|
|
|
|
|
|
canvas: (f32, f32),
|
|
|
|
|
|
) -> Element<'_, Message> {
|
2026-05-31 14:44:18 +03:00
|
|
|
|
const PANEL_BG: Color = Color { r: 0.16, g: 0.16, b: 0.16, a: 0.98 };
|
|
|
|
|
|
const BORDER: Color = Color { r: 0.40, g: 0.40, b: 0.40, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let field = text_input("Text", &ed.value)
|
|
|
|
|
|
.id(iced::widget::Id::new(TEXT_INLINE_ID))
|
|
|
|
|
|
.on_input(Message::TextInlineInput)
|
|
|
|
|
|
.on_submit(Message::TextInlineOk)
|
|
|
|
|
|
.padding(6)
|
|
|
|
|
|
.size(13)
|
|
|
|
|
|
.width(iced::Length::Fixed(240.0));
|
|
|
|
|
|
|
|
|
|
|
|
let panel = container(field)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 5.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(4);
|
|
|
|
|
|
|
2026-06-03 10:59:59 +03:00
|
|
|
|
// Keep the box on-screen so its field stays clickable at the edges.
|
|
|
|
|
|
const PANEL_W: f32 = 240.0 + 20.0;
|
|
|
|
|
|
const PANEL_H: f32 = 46.0;
|
|
|
|
|
|
let (cw, ch) = canvas;
|
2026-05-31 14:44:18 +03:00
|
|
|
|
let anchor = iced::Point::new(
|
2026-06-03 10:59:59 +03:00
|
|
|
|
(ed.screen_anchor.x - 6.0).clamp(0.0, (cw - PANEL_W).max(0.0)),
|
|
|
|
|
|
(ed.screen_anchor.y - 18.0).clamp(0.0, (ch - PANEL_H).max(0.0)),
|
2026-05-31 14:44:18 +03:00
|
|
|
|
);
|
|
|
|
|
|
position_canvas_overlay(anchor, panel.into())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
// Stroke-font families the renderer ships (LibreCAD LFF; see scene/lff.rs).
|
|
|
|
|
|
const MTEXT_FONTS: [&str; 10] = [
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
"[Style default]",
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
"Standard",
|
|
|
|
|
|
"ISO",
|
|
|
|
|
|
"Simplex",
|
|
|
|
|
|
"RomanS",
|
|
|
|
|
|
"RomanD",
|
|
|
|
|
|
"ItalicC",
|
|
|
|
|
|
"ScriptS",
|
|
|
|
|
|
"GothGBT",
|
|
|
|
|
|
"Cursive",
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
];
|
|
|
|
|
|
/// (label, ACI). 256 = ByLayer.
|
|
|
|
|
|
const MTEXT_COLORS: [(&str, u16); 8] = [
|
|
|
|
|
|
("ByLayer", 256),
|
|
|
|
|
|
("Red", 1),
|
|
|
|
|
|
("Yellow", 2),
|
|
|
|
|
|
("Green", 3),
|
|
|
|
|
|
("Cyan", 4),
|
|
|
|
|
|
("Blue", 5),
|
|
|
|
|
|
("Magenta", 6),
|
|
|
|
|
|
("White", 7),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/// Canvas program that renders the tessellated MText strokes inside the
|
|
|
|
|
|
/// editor's own preview area (never on the drawing). Strokes lie in the
|
|
|
|
|
|
/// world XY plane; the program fits + vertically flips them into the box.
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
const MTEXT_PREVIEW_PAD: f32 = 12.0;
|
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
struct MTextPreview {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
/// Disconnected polylines as (x, y) world points + colour (NaN-split done).
|
|
|
|
|
|
segments: Vec<(Vec<(f32, f32)>, Color)>,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
/// Per-visible-character boxes (world frame) for click-to-select.
|
|
|
|
|
|
boxes: Vec<crate::entities::text_support::GlyphBox>,
|
|
|
|
|
|
/// Current selection as a visible-char range.
|
|
|
|
|
|
sel: Option<(usize, usize)>,
|
2026-05-31 10:16:30 +03:00
|
|
|
|
/// Caret position as a visible-char offset.
|
|
|
|
|
|
caret: usize,
|
2026-05-31 13:14:35 +03:00
|
|
|
|
/// Whether the caret is in its visible blink phase.
|
|
|
|
|
|
caret_on: bool,
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
/// World-space min corner (bbox) and pixels-per-world-unit scale.
|
|
|
|
|
|
minx: f32,
|
|
|
|
|
|
miny: f32,
|
|
|
|
|
|
scale: f32,
|
|
|
|
|
|
content_h: f32,
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 02:33:47 +03:00
|
|
|
|
impl MTextPreview {
|
|
|
|
|
|
/// Visible-char offset (0..=N) nearest the cursor point (bounds-local px).
|
|
|
|
|
|
fn offset_at(&self, p: iced::Point) -> usize {
|
|
|
|
|
|
if self.boxes.is_empty() {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
let wx = self.minx + (p.x - MTEXT_PREVIEW_PAD) / self.scale;
|
|
|
|
|
|
let wy = self.miny + (self.content_h - p.y - MTEXT_PREVIEW_PAD) / self.scale;
|
|
|
|
|
|
let mut best = 0usize;
|
|
|
|
|
|
let mut best_d = f32::MAX;
|
|
|
|
|
|
for b in &self.boxes {
|
|
|
|
|
|
let dx = if wx < b.xmin {
|
|
|
|
|
|
b.xmin - wx
|
|
|
|
|
|
} else if wx > b.xmax {
|
|
|
|
|
|
wx - b.xmax
|
|
|
|
|
|
} else {
|
|
|
|
|
|
0.0
|
|
|
|
|
|
};
|
|
|
|
|
|
let dy = if wy < b.ymin {
|
|
|
|
|
|
b.ymin - wy
|
|
|
|
|
|
} else if wy > b.ymax {
|
|
|
|
|
|
wy - b.ymax
|
|
|
|
|
|
} else {
|
|
|
|
|
|
0.0
|
|
|
|
|
|
};
|
|
|
|
|
|
let d = dy * 1000.0 + dx; // prefer the correct line first
|
|
|
|
|
|
if d < best_d {
|
|
|
|
|
|
best_d = d;
|
|
|
|
|
|
best = b.vis;
|
|
|
|
|
|
// After the glyph centre → caret sits after this char.
|
|
|
|
|
|
if wx > (b.xmin + b.xmax) * 0.5 {
|
|
|
|
|
|
best = b.vis + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
best
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
|
struct MTextPreviewState {
|
|
|
|
|
|
dragging: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
impl iced::widget::canvas::Program<Message> for MTextPreview {
|
2026-05-31 02:33:47 +03:00
|
|
|
|
type State = MTextPreviewState;
|
|
|
|
|
|
|
|
|
|
|
|
fn update(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
state: &mut MTextPreviewState,
|
|
|
|
|
|
event: &iced::Event,
|
|
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
|
cursor: iced::mouse::Cursor,
|
|
|
|
|
|
) -> Option<iced::widget::canvas::Action<Message>> {
|
|
|
|
|
|
use iced::mouse::{Button, Event as Me};
|
|
|
|
|
|
use iced::widget::canvas::Action;
|
|
|
|
|
|
use iced::Event;
|
|
|
|
|
|
match event {
|
|
|
|
|
|
Event::Mouse(Me::ButtonPressed(Button::Left)) => {
|
|
|
|
|
|
if let Some(p) = cursor.position_in(bounds) {
|
|
|
|
|
|
state.dragging = true;
|
|
|
|
|
|
let off = self.offset_at(p);
|
|
|
|
|
|
return Some(Action::publish(Message::MTextSelStart(off)).and_capture());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Event::Mouse(Me::CursorMoved { .. }) => {
|
|
|
|
|
|
if state.dragging {
|
|
|
|
|
|
if let Some(p) = cursor.position_in(bounds) {
|
|
|
|
|
|
let off = self.offset_at(p);
|
|
|
|
|
|
return Some(Action::publish(Message::MTextSelTo(off)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Event::Mouse(Me::ButtonReleased(Button::Left)) => {
|
|
|
|
|
|
if state.dragging {
|
|
|
|
|
|
state.dragging = false;
|
|
|
|
|
|
return Some(Action::capture());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
fn draw(
|
|
|
|
|
|
&self,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
_state: &MTextPreviewState,
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
renderer: &iced::Renderer,
|
|
|
|
|
|
_theme: &Theme,
|
|
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
|
_cursor: iced::mouse::Cursor,
|
|
|
|
|
|
) -> Vec<iced::widget::canvas::Geometry> {
|
|
|
|
|
|
use iced::widget::canvas::{Frame, Path, Stroke};
|
|
|
|
|
|
let mut frame = Frame::new(renderer, bounds.size());
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
let pad = MTEXT_PREVIEW_PAD;
|
|
|
|
|
|
// Draw at the real size; flip Y (world up → screen down).
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
let map = |x: f32, y: f32| {
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
iced::Point::new(
|
|
|
|
|
|
pad + (x - self.minx) * self.scale,
|
|
|
|
|
|
self.content_h - (pad + (y - self.miny) * self.scale),
|
|
|
|
|
|
)
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
};
|
2026-05-31 02:33:47 +03:00
|
|
|
|
// Selection highlight behind the glyphs.
|
|
|
|
|
|
if let Some((a, b)) = self.sel {
|
|
|
|
|
|
for bx in &self.boxes {
|
|
|
|
|
|
if bx.vis >= a && bx.vis < b {
|
|
|
|
|
|
let p0 = map(bx.xmin, bx.ymax);
|
|
|
|
|
|
let p1 = map(bx.xmax, bx.ymin);
|
|
|
|
|
|
let rect = Path::rectangle(
|
|
|
|
|
|
iced::Point::new(p0.x.min(p1.x), p0.y.min(p1.y)),
|
|
|
|
|
|
iced::Size::new((p1.x - p0.x).abs(), (p1.y - p0.y).abs()),
|
|
|
|
|
|
);
|
|
|
|
|
|
frame.fill(&rect, Color { r: 0.20, g: 0.42, b: 0.72, a: 0.45 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
for (seg, col) in &self.segments {
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
if seg.len() < 2 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let path = Path::new(|p| {
|
|
|
|
|
|
p.move_to(map(seg[0].0, seg[0].1));
|
|
|
|
|
|
for &(x, y) in &seg[1..] {
|
|
|
|
|
|
p.line_to(map(x, y));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
frame.stroke(&path, Stroke::default().with_color(*col).with_width(1.4));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
}
|
2026-05-31 10:16:30 +03:00
|
|
|
|
// Caret — a vertical bar at the caret's glyph boundary, shown when the
|
|
|
|
|
|
// selection is empty (a plain text cursor).
|
2026-05-31 13:14:35 +03:00
|
|
|
|
// Caret is shown only when the selection is empty and the blink is in
|
|
|
|
|
|
// its visible phase.
|
|
|
|
|
|
let collapsed = self.caret_on && self.sel.map(|(a, b)| a == b).unwrap_or(true);
|
2026-05-31 10:16:30 +03:00
|
|
|
|
if collapsed && self.boxes.is_empty() {
|
|
|
|
|
|
// Empty text: show a caret at the top-left so the user can type.
|
|
|
|
|
|
let path = Path::new(|p| {
|
|
|
|
|
|
p.move_to(iced::Point::new(MTEXT_PREVIEW_PAD, MTEXT_PREVIEW_PAD));
|
|
|
|
|
|
p.line_to(iced::Point::new(
|
|
|
|
|
|
MTEXT_PREVIEW_PAD,
|
|
|
|
|
|
(MTEXT_PREVIEW_PAD + 22.0).min(self.content_h),
|
|
|
|
|
|
));
|
|
|
|
|
|
});
|
|
|
|
|
|
frame.stroke(
|
|
|
|
|
|
&path,
|
|
|
|
|
|
Stroke::default()
|
|
|
|
|
|
.with_color(Color { r: 0.95, g: 0.95, b: 0.55, a: 1.0 })
|
|
|
|
|
|
.with_width(1.5),
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if collapsed {
|
|
|
|
|
|
let bar = if let Some(b) = self.boxes.iter().find(|b| b.vis == self.caret) {
|
|
|
|
|
|
Some((b.xmin, b.ymin, b.ymax)) // left edge of the caret's glyph
|
|
|
|
|
|
} else if self.caret > 0 {
|
|
|
|
|
|
self.boxes
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|b| b.vis == self.caret - 1)
|
|
|
|
|
|
.map(|b| (b.xmax, b.ymin, b.ymax)) // after the last glyph
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.boxes.first().map(|b| (b.xmin, b.ymin, b.ymax))
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some((cx, y0, y1)) = bar {
|
|
|
|
|
|
let p0 = map(cx, y0);
|
|
|
|
|
|
let p1 = map(cx, y1);
|
|
|
|
|
|
let path = Path::new(|p| {
|
|
|
|
|
|
p.move_to(p0);
|
|
|
|
|
|
p.line_to(p1);
|
|
|
|
|
|
});
|
|
|
|
|
|
frame.stroke(
|
|
|
|
|
|
&path,
|
|
|
|
|
|
Stroke::default()
|
|
|
|
|
|
.with_color(Color { r: 0.95, g: 0.95, b: 0.55, a: 1.0 })
|
|
|
|
|
|
.with_width(1.5),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
vec![frame.into_geometry()]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
/// Split every preview WireModel into finite (x, y) polyline runs, each
|
|
|
|
|
|
/// carrying its wire's colour so inline `\C` / the colour dropdown shows.
|
|
|
|
|
|
fn mtext_preview_segments(
|
|
|
|
|
|
ed: &super::mtext_editor::MTextEditorState,
|
|
|
|
|
|
) -> Vec<(Vec<(f32, f32)>, Color)> {
|
|
|
|
|
|
let mut out: Vec<(Vec<(f32, f32)>, Color)> = Vec::new();
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
for w in &ed.preview_wires {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
let col = Color {
|
|
|
|
|
|
r: w.color[0],
|
|
|
|
|
|
g: w.color[1],
|
|
|
|
|
|
b: w.color[2],
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
let mut run: Vec<(f32, f32)> = Vec::new();
|
|
|
|
|
|
for p in &w.points {
|
|
|
|
|
|
if p[0].is_finite() && p[1].is_finite() {
|
|
|
|
|
|
run.push((p[0], p[1]));
|
|
|
|
|
|
} else if !run.is_empty() {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
out.push((std::mem::take(&mut run), col));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !run.is_empty() {
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
out.push((run, col));
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn mtext_editor_overlay<'a>(
|
|
|
|
|
|
ed: &'a super::mtext_editor::MTextEditorState,
|
|
|
|
|
|
styles: Vec<String>,
|
2026-06-03 10:59:59 +03:00
|
|
|
|
canvas_size: (f32, f32),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
use super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign};
|
|
|
|
|
|
use iced::widget::{canvas, svg, text_editor};
|
|
|
|
|
|
|
|
|
|
|
|
const PANEL_BG: Color = Color { r: 0.16, g: 0.16, b: 0.16, a: 0.98 };
|
|
|
|
|
|
const BORDER: Color = Color { r: 0.40, g: 0.40, b: 0.40, a: 1.0 };
|
|
|
|
|
|
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
|
|
|
|
|
const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let btn_style = |_: &Theme, status: button::Status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => {
|
|
|
|
|
|
Color { r: 0.28, g: 0.40, b: 0.55, a: 1.0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
let icon_btn = move |bytes: &'static [u8], msg: Message| -> Element<'static, Message> {
|
|
|
|
|
|
button(svg(svg::Handle::from_memory(bytes)).width(18).height(18))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.padding(3)
|
|
|
|
|
|
.style(btn_style)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
let lbl = |s: &'static str| text(s).size(11).color(TEXT_COL);
|
|
|
|
|
|
let small_input = |placeholder: &'static str,
|
|
|
|
|
|
val: &str,
|
|
|
|
|
|
on: fn(String) -> Message,
|
|
|
|
|
|
w: f32|
|
|
|
|
|
|
-> Element<'static, Message> {
|
|
|
|
|
|
text_input(placeholder, val)
|
|
|
|
|
|
.on_input(on)
|
|
|
|
|
|
.width(iced::Length::Fixed(w))
|
|
|
|
|
|
.padding(3)
|
|
|
|
|
|
.size(12)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Row 1: style / font / height · format icons · colour ──────────────
|
|
|
|
|
|
let style_opts: Vec<String> = if styles.is_empty() {
|
|
|
|
|
|
vec!["Standard".to_string()]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
styles
|
|
|
|
|
|
};
|
|
|
|
|
|
let style_pl = pick_list(style_opts, Some(ed.style.clone()), Message::MTextStyle)
|
|
|
|
|
|
.text_size(11)
|
|
|
|
|
|
.width(iced::Length::Fixed(96.0));
|
|
|
|
|
|
let font_sel = if ed.font.trim().is_empty() {
|
|
|
|
|
|
"[Style default]".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ed.font.clone()
|
|
|
|
|
|
};
|
|
|
|
|
|
let font_pl = pick_list(
|
|
|
|
|
|
MTEXT_FONTS.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
|
|
|
|
|
|
Some(font_sel),
|
|
|
|
|
|
Message::MTextFont,
|
|
|
|
|
|
)
|
|
|
|
|
|
.text_size(11)
|
|
|
|
|
|
.width(iced::Length::Fixed(120.0));
|
|
|
|
|
|
let color_sel = MTEXT_COLORS
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|(_, a)| *a == ed.color_aci)
|
|
|
|
|
|
.map(|(n, _)| n.to_string())
|
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string());
|
|
|
|
|
|
let color_pl = pick_list(
|
|
|
|
|
|
MTEXT_COLORS.iter().map(|(n, _)| n.to_string()).collect::<Vec<_>>(),
|
|
|
|
|
|
Some(color_sel),
|
|
|
|
|
|
|name: String| {
|
|
|
|
|
|
let aci = MTEXT_COLORS
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|(n, _)| *n == name)
|
|
|
|
|
|
.map(|(_, a)| *a)
|
|
|
|
|
|
.unwrap_or(256);
|
|
|
|
|
|
Message::MTextColor(aci)
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
.text_size(11)
|
|
|
|
|
|
.width(iced::Length::Fixed(96.0));
|
|
|
|
|
|
|
|
|
|
|
|
let row1 = row![
|
|
|
|
|
|
style_pl,
|
|
|
|
|
|
font_pl,
|
|
|
|
|
|
small_input("2.5", &ed.height, Message::MTextHeight, 64.0),
|
|
|
|
|
|
iced::widget::Space::new().width(6),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_bold.svg"), Message::MTextFmt(MTextFmt::Bold)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_italic.svg"), Message::MTextFmt(MTextFmt::Italic)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_underline.svg"), Message::MTextFmt(MTextFmt::Underline)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_overline.svg"), Message::MTextFmt(MTextFmt::Overline)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_strike.svg"), Message::MTextFmt(MTextFmt::Strike)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_upper.svg"), Message::MTextFmt(MTextFmt::Uppercase)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_lower.svg"), Message::MTextFmt(MTextFmt::Lowercase)),
|
|
|
|
|
|
iced::widget::Space::new().width(Fill),
|
|
|
|
|
|
color_pl,
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(4)
|
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Row 2: oblique / width / char-spacing · align · line spacing · OK ─
|
|
|
|
|
|
let justify = pick_list(
|
|
|
|
|
|
JustifyChoice::ALL,
|
|
|
|
|
|
Some(JustifyChoice(ed.attachment)),
|
|
|
|
|
|
|c| Message::MTextJustify(c.0),
|
|
|
|
|
|
)
|
|
|
|
|
|
.text_size(11)
|
|
|
|
|
|
.width(iced::Length::Fixed(112.0));
|
|
|
|
|
|
let row2 = row![
|
|
|
|
|
|
lbl("O"),
|
|
|
|
|
|
small_input("0", &ed.oblique, Message::MTextOblique, 48.0),
|
|
|
|
|
|
lbl("W"),
|
|
|
|
|
|
small_input("1", &ed.width, Message::MTextWidth, 48.0),
|
|
|
|
|
|
lbl("◊"),
|
|
|
|
|
|
small_input("0", &ed.char_space, Message::MTextCharSpace, 48.0),
|
|
|
|
|
|
iced::widget::Space::new().width(6),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_align_left.svg"), Message::MTextAlign(ParaAlign::Left)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_align_center.svg"), Message::MTextAlign(ParaAlign::Center)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_align_right.svg"), Message::MTextAlign(ParaAlign::Right)),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_align_justify.svg"), Message::MTextAlign(ParaAlign::Justify)),
|
|
|
|
|
|
iced::widget::Space::new().width(6),
|
|
|
|
|
|
justify,
|
|
|
|
|
|
lbl("LS"),
|
|
|
|
|
|
button(lbl("1")).on_press(Message::MTextLineSpacing(1.0)).padding(3).style(btn_style),
|
|
|
|
|
|
button(lbl("1.5")).on_press(Message::MTextLineSpacing(1.5)).padding(3).style(btn_style),
|
|
|
|
|
|
button(lbl("2")).on_press(Message::MTextLineSpacing(2.0)).padding(3).style(btn_style),
|
|
|
|
|
|
iced::widget::Space::new().width(Fill),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_ok.svg"), Message::MTextOk),
|
|
|
|
|
|
icon_btn(include_bytes!("../../assets/icons/mt_cancel.svg"), Message::MTextCancel),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(4)
|
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
|
2026-05-31 01:59:39 +03:00
|
|
|
|
// ── Segmented Edit | Preview toggle (between toolbar and body) ────────
|
|
|
|
|
|
let seg_btn = move |label: &'static str, active: bool, on: Message| -> Element<'static, Message> {
|
|
|
|
|
|
button(text(label).size(12).color(if active {
|
|
|
|
|
|
Color::WHITE
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color { r: 0.80, g: 0.80, b: 0.80, a: 1.0 }
|
|
|
|
|
|
}))
|
|
|
|
|
|
.on_press(on)
|
|
|
|
|
|
.padding([4, 14])
|
|
|
|
|
|
.style(move |_: &Theme, _| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(if active {
|
|
|
|
|
|
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
let toggle = container(
|
|
|
|
|
|
row![
|
|
|
|
|
|
seg_btn("Edit", !ed.show_preview, Message::MTextShowPreview(false)),
|
|
|
|
|
|
seg_btn("Preview", ed.show_preview, Message::MTextShowPreview(true)),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0),
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding([6, 0]);
|
|
|
|
|
|
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
// ── Body: toggles between raw code input and rendered preview ────────
|
|
|
|
|
|
const VIEW_H: f32 = 150.0;
|
|
|
|
|
|
let body: Element<'a, Message> = if ed.show_preview {
|
|
|
|
|
|
let segments = mtext_preview_segments(ed);
|
|
|
|
|
|
let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
feat(fonts): switch the text engine from CXF to LibreCAD LFF fonts
Replace the small QCAD CXF stroke-font set with the full LibreCAD LFF font
library (46 fonts) and a new LFF parser/engine in src/scene/lff.rs. The
parser reads the `[hex] char` glyph blocks, tessellates `,A<bulge>` arcs, and
resolves `C<hex>` glyph references. Per LibreCAD's RS_Polyline convention a
vertex's bulge curves the segment ending at that vertex — getting this wrong
turned round letters like O into sideways capsules.
Font name resolution mirrors LibreCAD: match by file stem, map common
AutoCAD/DXF SHX names (txt, isocp, complex, italic, gothic*, gost, …) to the
nearest LFF family, and fall back to `standard`. A missing glyph in the chosen
family falls back to the broad `unicode` font so accented/Cyrillic/Greek text
still renders. Cap height stays 9 units, so the text scale is unchanged.
All text consumers move from cxf:: to lff::; complex linetypes still render
embedded text but no longer draw embedded shapes (LibreCAD ships no shape
file). The CXF module and every *.cxf asset are removed, and the MText / text
style font pickers list the LFF families.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:31:20 +03:00
|
|
|
|
for (seg, _) in &segments {
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
for &(x, y) in seg {
|
|
|
|
|
|
minx = minx.min(x);
|
|
|
|
|
|
miny = miny.min(y);
|
|
|
|
|
|
maxx = maxx.max(x);
|
|
|
|
|
|
maxy = maxy.max(y);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-31 02:33:47 +03:00
|
|
|
|
// Include glyph boxes so all-whitespace / box-only lines still anchor
|
|
|
|
|
|
// the transform (hit-testing relies on minx/miny).
|
|
|
|
|
|
for b in &ed.glyph_boxes {
|
|
|
|
|
|
minx = minx.min(b.xmin);
|
|
|
|
|
|
miny = miny.min(b.ymin);
|
|
|
|
|
|
maxx = maxx.max(b.xmax);
|
|
|
|
|
|
maxy = maxy.max(b.ymax);
|
|
|
|
|
|
}
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
let h_unit = ed.height_value() as f32;
|
|
|
|
|
|
// Real text size: fixed pixels per em so more/taller text grows the
|
|
|
|
|
|
// canvas (and scrolls) instead of shrinking to fit.
|
|
|
|
|
|
let scale = (22.0 / h_unit.max(1e-3)).clamp(2.0, 600.0);
|
|
|
|
|
|
let content_h = if maxx >= minx {
|
|
|
|
|
|
((maxy - miny) * scale + 2.0 * MTEXT_PREVIEW_PAD).max(40.0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
40.0
|
|
|
|
|
|
};
|
2026-05-31 02:33:47 +03:00
|
|
|
|
let prog = MTextPreview {
|
|
|
|
|
|
segments,
|
|
|
|
|
|
boxes: ed.glyph_boxes.clone(),
|
|
|
|
|
|
sel: ed.sel,
|
2026-05-31 10:16:30 +03:00
|
|
|
|
caret: ed.caret,
|
2026-05-31 13:14:35 +03:00
|
|
|
|
caret_on: ed.caret_blink_on,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
minx,
|
|
|
|
|
|
miny,
|
|
|
|
|
|
scale,
|
|
|
|
|
|
content_h,
|
|
|
|
|
|
};
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
let cv = canvas(prog).width(Fill).height(iced::Length::Fixed(content_h));
|
|
|
|
|
|
container(iced::widget::scrollable(cv).height(iced::Length::Fixed(VIEW_H)))
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(FIELD_BG)),
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(2)
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
.width(Fill)
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
.into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
text_editor(&ed.content)
|
2026-05-31 10:16:30 +03:00
|
|
|
|
.id(iced::widget::Id::new(MTEXT_TEXT_ID))
|
feat: Implement in-place MText editor with formatting toolbar and live preview
MTEXT (and double-clicking an MText) now opens an in-place editor anchored
at the insertion point instead of typing a single line on the command line.
The editor has a two-row formatting toolbar: text style / font / height,
bold, italic, underline, overline, strike, upper/lowercase and a colour
dropdown on top; oblique angle, width factor, character spacing, paragraph
alignment, justification, line spacing and OK/Cancel below. Character
toggles wrap the selection in DXF inline codes; global fields apply as
leading runs or entity properties.
Editing is shown in the editor's own area, not on the drawing: a Code/Preview
toggle switches between the raw text input and a rendered preview canvas that
draws the tessellated glyph strokes at real size, scrolling when the text
outgrows the view. The MText box keeps a fixed width so text wraps to the
next line instead of stretching into one line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 01:51:20 +03:00
|
|
|
|
.on_action(Message::MTextEdit)
|
|
|
|
|
|
.height(iced::Length::Fixed(VIEW_H))
|
|
|
|
|
|
.padding(6)
|
|
|
|
|
|
.size(13)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-31 01:59:39 +03:00
|
|
|
|
let panel = container(column![row1, row2, toggle, body].spacing(5))
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
|
|
|
|
border: Border { color: BORDER, width: 1.0, radius: 5.0.into() },
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(6)
|
|
|
|
|
|
.width(iced::Length::Fixed(640.0));
|
|
|
|
|
|
|
2026-06-03 10:59:59 +03:00
|
|
|
|
// Keep the whole panel on-screen: clamp the anchor so it never spills past
|
|
|
|
|
|
// the right/bottom edge (where its toolbar buttons would be unclickable).
|
|
|
|
|
|
// Width is fixed; height is the toolbar rows + the fixed VIEW_H body.
|
|
|
|
|
|
const PANEL_W: f32 = 640.0 + 14.0; // fixed width + padding/border
|
|
|
|
|
|
const PANEL_H: f32 = VIEW_H + 150.0; // body + toolbars/toggle/padding
|
|
|
|
|
|
let (cw, ch) = canvas_size;
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
let anchor = iced::Point::new(
|
2026-06-03 10:59:59 +03:00
|
|
|
|
(ed.screen_anchor.x - 10.0).clamp(0.0, (cw - PANEL_W).max(0.0)),
|
|
|
|
|
|
(ed.screen_anchor.y - 90.0).clamp(0.0, (ch - PANEL_H).max(0.0)),
|
feat: Implement in-place MText editor with formatting toolbar and live preview
- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
2026-05-30 23:24:06 +03:00
|
|
|
|
);
|
|
|
|
|
|
position_canvas_overlay(anchor, panel.into())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 00:41:01 +03:00
|
|
|
|
// ── Viewport right-click context menu ──────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
fn viewport_context_menu_overlay(
|
|
|
|
|
|
pos: iced::Point,
|
|
|
|
|
|
has_cmd: bool,
|
|
|
|
|
|
has_selection: bool,
|
2026-06-04 23:17:21 +03:00
|
|
|
|
isolation_active: bool,
|
2026-04-08 00:41:01 +03:00
|
|
|
|
last_cmds: Vec<String>,
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
draworder_open: bool,
|
2026-04-08 00:41:01 +03:00
|
|
|
|
) -> Element<'static, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
const MENU_BG: Color = Color {
|
|
|
|
|
|
r: 0.17,
|
|
|
|
|
|
g: 0.17,
|
|
|
|
|
|
b: 0.17,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const MENU_BORDER: Color = Color {
|
|
|
|
|
|
r: 0.35,
|
|
|
|
|
|
g: 0.35,
|
|
|
|
|
|
b: 0.35,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const ITEM_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.25,
|
|
|
|
|
|
g: 0.45,
|
|
|
|
|
|
b: 0.70,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TEXT_COL: Color = Color {
|
|
|
|
|
|
r: 0.88,
|
|
|
|
|
|
g: 0.88,
|
|
|
|
|
|
b: 0.88,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const SEP_COL: Color = Color {
|
|
|
|
|
|
r: 0.30,
|
|
|
|
|
|
g: 0.30,
|
|
|
|
|
|
b: 0.30,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
2026-04-08 00:41:01 +03:00
|
|
|
|
|
|
|
|
|
|
let item = |label: String, msg: Message| -> Element<'static, Message> {
|
|
|
|
|
|
button(text(label).size(12).color(TEXT_COL))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let sep = || -> Element<'static, Message> {
|
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(1))
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(SEP_COL)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(1)
|
|
|
|
|
|
.padding([0, 6])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
// Indented variant for sub-menu rows (e.g. Draw Order children).
|
|
|
|
|
|
let subitem = |label: String, msg: Message| -> Element<'static, Message> {
|
|
|
|
|
|
button(text(label).size(12).color(TEXT_COL))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
top: 4.0,
|
|
|
|
|
|
right: 12.0,
|
|
|
|
|
|
bottom: 4.0,
|
|
|
|
|
|
left: 26.0,
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-08 00:41:01 +03:00
|
|
|
|
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
if has_cmd {
|
|
|
|
|
|
items.push(item("Cancel".to_string(), Message::CommandEscape));
|
|
|
|
|
|
items.push(item("Enter".to_string(), Message::CommandFinalize));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if !last_cmds.is_empty() {
|
|
|
|
|
|
let last = last_cmds[0].clone();
|
|
|
|
|
|
items.push(item(
|
|
|
|
|
|
format!("Repeat {last}"),
|
|
|
|
|
|
Message::Command(last.to_uppercase()),
|
|
|
|
|
|
));
|
|
|
|
|
|
if last_cmds.len() > 1 {
|
|
|
|
|
|
for cmd in last_cmds.iter().skip(1) {
|
|
|
|
|
|
let c = cmd.clone();
|
|
|
|
|
|
items.push(item(c.clone(), Message::Command(c.to_uppercase())));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
items.push(sep());
|
|
|
|
|
|
}
|
|
|
|
|
|
if has_selection {
|
|
|
|
|
|
items.push(item("Delete".to_string(), Message::DeleteSelected));
|
2026-05-12 10:36:15 +03:00
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Move".to_string(),
|
|
|
|
|
|
Message::Command("MOVE".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Copy".to_string(),
|
|
|
|
|
|
Message::Command("COPY".to_string()),
|
|
|
|
|
|
));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
items.push(sep());
|
feat(draworder): cross-type draw order + context menu
DRAWORDER now affects rendering immediately (bump geometry epoch so the
sort cache rebuilds) and front/back anchor to the block's full effective
sort range instead of only existing table entries, so bring-to-front
actually lands on top. The render fallback sorts table-absent entities by
their own handle.
Draw order now applies across entity types, not just within a type. A
per-entity draw-order rank (SortEntitiesTable override or own handle,
ranked per block) is encoded as a small signed clip-z bias in the wire,
hatch, image, and 3DFACE pipelines. The shared LessEqual depth buffer then
orders fills, lines, rasters and 2D solids against each other regardless of
pass order — without touching model coordinates. 3D meshes keep real
geometric depth; wipeouts stay always-on-top.
Adds a Draw Order submenu to the viewport context menu (Bring to Front,
Send to Back, Bring Above/Send Under Object). The above/under items run an
interactive reference-object pick, then reorder the captured selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:29:01 +03:00
|
|
|
|
let arrow = if draworder_open { "Draw Order \u{25be}" } else { "Draw Order \u{25b8}" };
|
|
|
|
|
|
items.push(item(arrow.to_string(), Message::DrawOrderSubmenuToggle));
|
|
|
|
|
|
if draworder_open {
|
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
|
"Bring to Front".to_string(),
|
|
|
|
|
|
Message::Command("DRAWORDER F".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
|
"Send to Back".to_string(),
|
|
|
|
|
|
Message::Command("DRAWORDER B".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
|
"Bring Above Object".to_string(),
|
|
|
|
|
|
Message::DrawOrderPickRef(true),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(subitem(
|
|
|
|
|
|
"Send Under Object".to_string(),
|
|
|
|
|
|
Message::DrawOrderPickRef(false),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
items.push(sep());
|
2026-06-04 23:17:21 +03:00
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Isolate Objects".to_string(),
|
|
|
|
|
|
Message::Command("ISOLATEOBJECTS".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Hide Objects".to_string(),
|
|
|
|
|
|
Message::Command("HIDEOBJECTS".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
items.push(sep());
|
2026-05-29 20:30:56 +03:00
|
|
|
|
items.push(item("Select Similar".to_string(), Message::SelectSimilar));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
}
|
2026-06-04 23:17:21 +03:00
|
|
|
|
if isolation_active {
|
|
|
|
|
|
items.push(item(
|
|
|
|
|
|
"End Object Isolation".to_string(),
|
|
|
|
|
|
Message::Command("UNISOLATEOBJECTS".to_string()),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Select All".to_string(),
|
|
|
|
|
|
Message::Command("SELECTALL".to_string()),
|
|
|
|
|
|
));
|
2026-05-29 20:30:56 +03:00
|
|
|
|
items.push(item("Quick Select...".to_string(), Message::QSelectOpen));
|
2026-05-12 10:36:15 +03:00
|
|
|
|
items.push(item(
|
|
|
|
|
|
"Zoom Extents".to_string(),
|
2026-05-30 21:40:48 +03:00
|
|
|
|
Message::Command("ZOOM EXTENTS".to_string()),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
|
let menu_col = column(items).spacing(0).width(iced::Length::Fixed(180.0));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
|
|
|
|
|
|
let menu = container(menu_col)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(MENU_BG)),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
border: Border {
|
|
|
|
|
|
color: MENU_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
2026-04-08 00:41:01 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
2026-05-29 15:58:23 +03:00
|
|
|
|
.padding([4, 0])
|
|
|
|
|
|
.width(iced::Length::Fixed(180.0));
|
2026-04-08 00:41:01 +03:00
|
|
|
|
|
2026-05-29 15:58:23 +03:00
|
|
|
|
position_canvas_overlay(pos, menu.into())
|
2026-04-08 00:41:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 07:13:36 +03:00
|
|
|
|
/// A small right-click context menu rendered above the status bar.
|
|
|
|
|
|
/// The `name` is the layout tab that was right-clicked.
|
|
|
|
|
|
fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
const MENU_BG: Color = Color {
|
|
|
|
|
|
r: 0.17,
|
|
|
|
|
|
g: 0.17,
|
|
|
|
|
|
b: 0.17,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const MENU_BORDER: Color = Color {
|
|
|
|
|
|
r: 0.35,
|
|
|
|
|
|
g: 0.35,
|
|
|
|
|
|
b: 0.35,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const ITEM_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.25,
|
|
|
|
|
|
g: 0.45,
|
|
|
|
|
|
b: 0.70,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TEXT_COLOR: Color = Color {
|
|
|
|
|
|
r: 0.88,
|
|
|
|
|
|
g: 0.88,
|
|
|
|
|
|
b: 0.88,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
|
|
let item = |label: &'static str, msg: Message| {
|
|
|
|
|
|
button(text(label).size(12).color(TEXT_COLOR))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COLOR,
|
|
|
|
|
|
border: Border::default(),
|
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 12])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let rename_name = name.to_string();
|
|
|
|
|
|
let delete_name = name.to_string();
|
|
|
|
|
|
|
|
|
|
|
|
let menu = container(
|
|
|
|
|
|
column![
|
2026-03-31 08:18:29 +03:00
|
|
|
|
item("Rename", Message::LayoutRenameStart(rename_name)),
|
|
|
|
|
|
item("Delete", Message::LayoutDelete(delete_name)),
|
2026-03-31 07:13:36 +03:00
|
|
|
|
]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.width(160),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(MENU_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: MENU_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 0]);
|
|
|
|
|
|
|
|
|
|
|
|
// Click-catcher fills the whole screen to close the menu when clicking outside.
|
|
|
|
|
|
let catcher = mouse_area(
|
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(Fill))
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::LayoutContextMenuClose)
|
|
|
|
|
|
.on_right_press(Message::LayoutContextMenuClose);
|
|
|
|
|
|
|
|
|
|
|
|
// Position the menu above the status bar at the left.
|
|
|
|
|
|
let positioned = container(menu)
|
|
|
|
|
|
.align_bottom(Fill)
|
|
|
|
|
|
.align_left(Fill)
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.padding(iced::Padding {
|
|
|
|
|
|
top: 0.0,
|
|
|
|
|
|
right: 0.0,
|
|
|
|
|
|
bottom: 30.0,
|
|
|
|
|
|
left: 8.0,
|
|
|
|
|
|
});
|
2026-03-31 07:13:36 +03:00
|
|
|
|
|
|
|
|
|
|
stack![catcher, positioned].into()
|
|
|
|
|
|
}
|
2026-03-31 08:05:17 +03:00
|
|
|
|
|
2026-05-29 20:30:56 +03:00
|
|
|
|
// ── Quick Select panel ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
const QSELECT_ANY_TYPE: &str = "(Any type)";
|
|
|
|
|
|
const QSELECT_ANY_PROP: &str = "(Any property)";
|
2026-05-29 20:30:56 +03:00
|
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
/// Floating panel for the Quick Select feature. Single-row filter:
|
|
|
|
|
|
/// object type → property → operator → value, plus an "Append to current
|
|
|
|
|
|
/// selection" checkbox. The property dropdown is type-aware — Common
|
|
|
|
|
|
/// properties (Layer, Color, Linetype, Lineweight) are always shown;
|
|
|
|
|
|
/// picking a specific Object type adds that type's `geometry_properties`
|
|
|
|
|
|
/// fields (Start X, Length, Radius, …) so type-specific filtering works.
|
2026-05-29 20:30:56 +03:00
|
|
|
|
fn qselect_overlay<'a>(
|
|
|
|
|
|
state: &'a crate::app::QSelectState,
|
|
|
|
|
|
types: &[&'static str],
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
properties: &[(String, String)],
|
2026-05-29 20:30:56 +03:00
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
use iced::widget::{checkbox, pick_list};
|
|
|
|
|
|
const BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 0.98 };
|
|
|
|
|
|
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
|
|
|
|
|
const TEXT: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
|
|
|
|
|
const BTN_OK: Color = Color { r: 0.22, g: 0.42, b: 0.68, a: 1.0 };
|
|
|
|
|
|
const BTN_OK_HOV: Color = Color { r: 0.30, g: 0.52, b: 0.80, a: 1.0 };
|
|
|
|
|
|
const BTN_BG: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
|
|
|
|
|
const BTN_HOV: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 };
|
|
|
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
|
2026-05-29 20:30:56 +03:00
|
|
|
|
type_options.extend(types.iter().map(|s| (*s).to_string()));
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
|
|
|
|
|
|
let mut prop_options: Vec<crate::app::QSelectPropertyChoice> =
|
|
|
|
|
|
vec![crate::app::QSelectPropertyChoice {
|
|
|
|
|
|
field: String::new(),
|
|
|
|
|
|
label: QSELECT_ANY_PROP.to_string(),
|
|
|
|
|
|
}];
|
|
|
|
|
|
prop_options.extend(properties.iter().map(|(field, label)| {
|
|
|
|
|
|
crate::app::QSelectPropertyChoice {
|
|
|
|
|
|
field: field.clone(),
|
|
|
|
|
|
label: label.clone(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
let op_options: Vec<crate::app::QSelectOp> = vec![
|
|
|
|
|
|
crate::app::QSelectOp::Eq,
|
|
|
|
|
|
crate::app::QSelectOp::Neq,
|
|
|
|
|
|
crate::app::QSelectOp::Gt,
|
|
|
|
|
|
crate::app::QSelectOp::Lt,
|
|
|
|
|
|
crate::app::QSelectOp::Any,
|
|
|
|
|
|
];
|
2026-05-29 20:30:56 +03:00
|
|
|
|
|
|
|
|
|
|
let type_sel = state
|
|
|
|
|
|
.type_filter
|
|
|
|
|
|
.clone()
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
.unwrap_or_else(|| QSELECT_ANY_TYPE.to_string());
|
|
|
|
|
|
let prop_sel = state.property.clone().unwrap_or(crate::app::QSelectPropertyChoice {
|
|
|
|
|
|
field: String::new(),
|
|
|
|
|
|
label: QSELECT_ANY_PROP.to_string(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// The value field is disabled (visually de-emphasised; we still
|
|
|
|
|
|
// render the same widget) when no property is picked or the
|
|
|
|
|
|
// operator is "*Any value" — both of those skip the value test.
|
|
|
|
|
|
let value_enabled = state.property.is_some()
|
|
|
|
|
|
&& !matches!(state.operator, crate::app::QSelectOp::Any);
|
2026-05-29 20:30:56 +03:00
|
|
|
|
|
|
|
|
|
|
let label = |s: &'static str| text(s).size(12).color(TEXT).width(iced::Length::Fixed(90.0));
|
|
|
|
|
|
|
|
|
|
|
|
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
|
button(text(lbl).size(12).color(TEXT))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(move |_: &Theme, st| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
|
|
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
|
|
|
|
|
hov
|
|
|
|
|
|
} else {
|
|
|
|
|
|
base
|
|
|
|
|
|
},
|
|
|
|
|
|
)),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 14])
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
let mut value_input = text_input("", &state.value).size(12);
|
|
|
|
|
|
if value_enabled {
|
|
|
|
|
|
value_input = value_input.on_input(Message::QSelectSetValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 20:30:56 +03:00
|
|
|
|
let panel_body = column![
|
|
|
|
|
|
text("Quick Select").size(14).color(TEXT),
|
|
|
|
|
|
Space::new().height(10),
|
|
|
|
|
|
row![
|
|
|
|
|
|
label("Object type:"),
|
|
|
|
|
|
pick_list(type_options, Some(type_sel), |s: String| {
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
if s == QSELECT_ANY_TYPE {
|
2026-05-29 20:30:56 +03:00
|
|
|
|
Message::QSelectSetType(None)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Message::QSelectSetType(Some(s))
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(8),
|
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
|
row![
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
label("Property:"),
|
|
|
|
|
|
pick_list(prop_options, Some(prop_sel), |p: crate::app::QSelectPropertyChoice| {
|
|
|
|
|
|
if p.field.is_empty() {
|
|
|
|
|
|
Message::QSelectSetProperty(None)
|
2026-05-29 20:30:56 +03:00
|
|
|
|
} else {
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
Message::QSelectSetProperty(Some(p))
|
2026-05-29 20:30:56 +03:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(8),
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
Space::new().height(6),
|
|
|
|
|
|
row![
|
|
|
|
|
|
label("Operator:"),
|
|
|
|
|
|
pick_list(op_options, Some(state.operator), Message::QSelectSetOperator)
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(8),
|
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
|
row![label("Value:"), value_input,]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(8),
|
2026-05-29 20:30:56 +03:00
|
|
|
|
Space::new().height(10),
|
|
|
|
|
|
row![
|
|
|
|
|
|
checkbox(state.append)
|
|
|
|
|
|
.on_toggle(Message::QSelectSetAppend)
|
|
|
|
|
|
.size(14),
|
|
|
|
|
|
Space::new().width(6),
|
|
|
|
|
|
text("Append to current selection").size(12).color(TEXT),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
|
Space::new().height(14),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(Fill),
|
|
|
|
|
|
btn("Cancel", Message::QSelectClose, BTN_BG, BTN_HOV),
|
|
|
|
|
|
Space::new().width(8),
|
|
|
|
|
|
btn("Apply", Message::QSelectApply, BTN_OK, BTN_OK_HOV),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0);
|
|
|
|
|
|
|
|
|
|
|
|
let panel = container(panel_body)
|
|
|
|
|
|
.padding(16)
|
feat(qselect): per-property filter with operator + value (#28)
Quick Select used to filter only on Object Type and Layer. The classic
QSELECT model — pick a type, pick one of its properties, compare with a
typed value via a logical operator — is much more general: a Line entity
can now be filtered on Start X, Length, Angle, Delta Y, …, and any
entity on the common Layer / Color / Linetype / Lineweight.
Panel layout: Object type → Property → Operator → Value → Append. The
Property dropdown is type-aware: it always lists the four common
properties; picking a specific Object type appends that type's
`geometry_properties` rows, sampled from the first entity of that type
in the active layout (so `Start X`, `Radius`, `Pattern Angle`, … only
appear when their entity type is selected). Changing the Object type
drops the Property when the new type no longer offers it.
Operators: `= Equals`, `!= Not equal`, `> Greater than`, `< Less than`,
`* Any value`. Eq/Neq do case-insensitive string compare against the
canonical property string (so "ByLayer" matches "bylayer"); Gt/Lt parse
both sides as `f64` and reject anything non-numeric; Any skips the
value test entirely. The Value text field is disabled when no Property
is picked or the operator is Any. Append behaves the same as before.
The new value reader is hand-rolled for the four common properties
(layer name, color formatted as `ByLayer`/`ByBlock`/`<aci>`/`r,g,b`,
linetype name, lineweight formatted as `0.30mm`) and falls back through
`geometry_properties()` for type-specific fields — extracting the
property's value out of the `PropValue` variant so EditText / Choice /
ColorChoice all canonicalise the same way the panel will display them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:09:56 +03:00
|
|
|
|
.width(iced::Length::Fixed(400.0))
|
2026-05-29 20:30:56 +03:00
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Outside-click catcher — fills the whole screen, sits below the
|
|
|
|
|
|
// panel. The panel itself is rendered above and absorbs its own
|
|
|
|
|
|
// clicks via standard widget event handling.
|
|
|
|
|
|
let catcher = mouse_area(
|
|
|
|
|
|
container(iced::widget::Space::new().width(Fill).height(Fill))
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::QSelectClose)
|
|
|
|
|
|
.on_right_press(Message::QSelectClose);
|
|
|
|
|
|
|
|
|
|
|
|
let centered = container(iced::widget::opaque(panel))
|
|
|
|
|
|
.center(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
stack![catcher, centered].into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 11:31:11 +03:00
|
|
|
|
/// Content for the floating "Unsaved Changes" OS window.
|
2026-05-01 16:12:32 +03:00
|
|
|
|
const SAVE_FORMAT_OPTIONS: &[&str] = &[
|
2026-05-12 10:36:15 +03:00
|
|
|
|
"DWG 2018", "DWG 2013", "DWG 2010", "DWG 2007", "DWG 2004", "DWG 2000", "DWG R14", "DXF 2018",
|
|
|
|
|
|
"DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14",
|
2026-05-01 16:12:32 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
fn save_as_dialog_window<'a>(
|
|
|
|
|
|
filename: &'a str,
|
2026-05-01 16:20:38 +03:00
|
|
|
|
folder: &'a std::path::Path,
|
|
|
|
|
|
entries: &'a [(String, bool, std::path::PathBuf)],
|
2026-05-01 16:12:32 +03:00
|
|
|
|
format: &'a str,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
const BG: Color = Color {
|
|
|
|
|
|
r: 0.15,
|
|
|
|
|
|
g: 0.15,
|
|
|
|
|
|
b: 0.17,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const LIST_BG: Color = Color {
|
|
|
|
|
|
r: 0.11,
|
|
|
|
|
|
g: 0.11,
|
|
|
|
|
|
b: 0.13,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BORDER: Color = Color {
|
|
|
|
|
|
r: 0.32,
|
|
|
|
|
|
g: 0.32,
|
|
|
|
|
|
b: 0.36,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TEXT: Color = Color {
|
|
|
|
|
|
r: 0.90,
|
|
|
|
|
|
g: 0.90,
|
|
|
|
|
|
b: 0.90,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const DIM: Color = Color {
|
|
|
|
|
|
r: 0.58,
|
|
|
|
|
|
g: 0.58,
|
|
|
|
|
|
b: 0.62,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const INPUT_BG: Color = Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.12,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_OK: Color = Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.46,
|
|
|
|
|
|
b: 0.80,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_HOV: Color = Color {
|
|
|
|
|
|
r: 0.26,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.92,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_GREY: Color = Color {
|
|
|
|
|
|
r: 0.26,
|
|
|
|
|
|
g: 0.26,
|
|
|
|
|
|
b: 0.29,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_GHOV: Color = Color {
|
|
|
|
|
|
r: 0.34,
|
|
|
|
|
|
g: 0.34,
|
|
|
|
|
|
b: 0.38,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const DIR_COL: Color = Color {
|
|
|
|
|
|
r: 0.75,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 1.00,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const FILE_COL: Color = TEXT;
|
|
|
|
|
|
const ROW_HOV: Color = Color {
|
|
|
|
|
|
r: 0.22,
|
|
|
|
|
|
g: 0.24,
|
|
|
|
|
|
b: 0.28,
|
|
|
|
|
|
a: 1.0,
|
2026-05-01 16:12:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let input_sty =
|
|
|
|
|
|
|_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
|
|
|
|
|
|
background: Background::Color(INPUT_BG),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
icon: TEXT,
|
|
|
|
|
|
placeholder: DIM,
|
|
|
|
|
|
value: TEXT,
|
|
|
|
|
|
selection: Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.46,
|
|
|
|
|
|
b: 0.80,
|
|
|
|
|
|
a: 0.45,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
|
button(text(lbl).size(12).color(TEXT))
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.on_press(msg)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(move |_: &Theme, st| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
|
|
|
|
|
hov
|
|
|
|
|
|
} else {
|
|
|
|
|
|
base
|
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
|
)),
|
|
|
|
|
|
text_color: TEXT,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
|
..Default::default()
|
2026-05-01 16:12:32 +03:00
|
|
|
|
})
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.padding([4, 12])
|
2026-05-01 16:12:32 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
// ── Path bar ─────────────────────────────────────────────────────────
|
|
|
|
|
|
let path_str = folder.to_string_lossy().into_owned();
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let up_path = folder.parent().map(|p| p.to_path_buf());
|
2026-05-01 16:20:38 +03:00
|
|
|
|
let path_bar = row![
|
|
|
|
|
|
{
|
|
|
|
|
|
let up_msg = up_path.map(Message::SaveDialogNavigate);
|
|
|
|
|
|
let b = button(text("↑").size(14).color(TEXT))
|
|
|
|
|
|
.style(|_: &Theme, st| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
|
|
|
|
|
BTN_GHOV
|
|
|
|
|
|
} else {
|
|
|
|
|
|
BTN_GREY
|
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
|
)),
|
|
|
|
|
|
text_color: TEXT,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([3, 10]);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if let Some(msg) = up_msg {
|
|
|
|
|
|
b.on_press(msg)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
b
|
|
|
|
|
|
}
|
2026-05-01 16:20:38 +03:00
|
|
|
|
},
|
|
|
|
|
|
Space::new().width(8),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
container(text(path_str.clone()).size(12).color(DIM))
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(INPUT_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into()
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.width(Fill),
|
2026-05-01 16:20:38 +03:00
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center);
|
|
|
|
|
|
|
|
|
|
|
|
// ── File list ─────────────────────────────────────────────────────────
|
|
|
|
|
|
let file_list: Element<'_, Message> = {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let rows: Vec<Element<'_, Message>> = entries
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(name, is_dir, path)| {
|
|
|
|
|
|
let icon = if *is_dir { "📁" } else { "📄" };
|
|
|
|
|
|
let color = if *is_dir { DIR_COL } else { FILE_COL };
|
|
|
|
|
|
let p = path.clone();
|
|
|
|
|
|
let d = *is_dir;
|
|
|
|
|
|
mouse_area(
|
|
|
|
|
|
container(
|
|
|
|
|
|
row![
|
|
|
|
|
|
text(icon).size(13),
|
|
|
|
|
|
Space::new().width(6),
|
|
|
|
|
|
text(name.as_str()).size(13).color(color),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([3, 8])
|
|
|
|
|
|
.width(Fill),
|
2026-05-01 16:20:38 +03:00
|
|
|
|
)
|
2026-05-12 10:36:15 +03:00
|
|
|
|
.on_press(Message::SaveDialogEntryClicked(p, d))
|
|
|
|
|
|
.into()
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
2026-05-01 16:12:32 +03:00
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
container(iced::widget::scrollable(
|
|
|
|
|
|
column(rows).spacing(1).width(Fill),
|
|
|
|
|
|
))
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(LIST_BG)),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
2026-05-01 16:20:38 +03:00
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
};
|
|
|
|
|
|
let _ = ROW_HOV; // used conceptually, suppress warning
|
2026-05-01 16:12:32 +03:00
|
|
|
|
|
2026-05-01 16:20:38 +03:00
|
|
|
|
let sel_fmt = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let label = |s: &'static str| text(s).size(11).color(DIM);
|
2026-05-01 16:20:38 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Bottom controls ───────────────────────────────────────────────────
|
|
|
|
|
|
let bottom = column![
|
|
|
|
|
|
row![
|
|
|
|
|
|
label("File name:").width(90),
|
|
|
|
|
|
text_input("drawing.dwg", filename)
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.on_input(Message::SaveDialogFilenameChanged)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(input_sty)
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.size(13)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.padding([5, 8])
|
|
|
|
|
|
.width(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(6),
|
|
|
|
|
|
Space::new().height(6),
|
|
|
|
|
|
row![
|
|
|
|
|
|
label("Format:").width(90),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
pick_list(SAVE_FORMAT_OPTIONS, sel_fmt, |s: &str| {
|
|
|
|
|
|
Message::SaveDialogFormatChanged(s.to_string())
|
|
|
|
|
|
})
|
|
|
|
|
|
.width(Fill),
|
2026-05-01 16:20:38 +03:00
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Alignment::Center)
|
|
|
|
|
|
.spacing(6),
|
|
|
|
|
|
Space::new().height(12),
|
|
|
|
|
|
row![
|
|
|
|
|
|
Space::new().width(Fill),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
|
2026-05-01 16:20:38 +03:00
|
|
|
|
Space::new().width(8),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
|
2026-05-01 16:20:38 +03:00
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0);
|
|
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
|
column![
|
|
|
|
|
|
path_bar,
|
|
|
|
|
|
Space::new().height(8),
|
|
|
|
|
|
file_list,
|
|
|
|
|
|
Space::new().height(10),
|
|
|
|
|
|
bottom,
|
2026-05-01 16:12:32 +03:00
|
|
|
|
]
|
|
|
|
|
|
.spacing(0),
|
|
|
|
|
|
)
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.style(|_: &Theme| container::Style {
|
2026-05-01 16:12:32 +03:00
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
2026-05-01 16:20:38 +03:00
|
|
|
|
.padding([14, 16])
|
2026-05-01 16:12:32 +03:00
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 11:31:11 +03:00
|
|
|
|
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
const BG: Color = Color {
|
|
|
|
|
|
r: 0.18,
|
|
|
|
|
|
g: 0.18,
|
|
|
|
|
|
b: 0.20,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BORDER_COL: Color = Color {
|
|
|
|
|
|
r: 0.38,
|
|
|
|
|
|
g: 0.38,
|
|
|
|
|
|
b: 0.42,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const TEXT_COL: Color = Color {
|
|
|
|
|
|
r: 0.90,
|
|
|
|
|
|
g: 0.90,
|
|
|
|
|
|
b: 0.90,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_SAVE: Color = Color {
|
|
|
|
|
|
r: 0.20,
|
|
|
|
|
|
g: 0.46,
|
|
|
|
|
|
b: 0.80,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_HOVER: Color = Color {
|
|
|
|
|
|
r: 0.26,
|
|
|
|
|
|
g: 0.55,
|
|
|
|
|
|
b: 0.92,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_DISC: Color = Color {
|
|
|
|
|
|
r: 0.28,
|
|
|
|
|
|
g: 0.28,
|
|
|
|
|
|
b: 0.30,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
const BTN_DHOV: Color = Color {
|
|
|
|
|
|
r: 0.36,
|
|
|
|
|
|
g: 0.36,
|
|
|
|
|
|
b: 0.40,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
};
|
2026-05-01 11:31:11 +03:00
|
|
|
|
|
|
|
|
|
|
let body_text = format!("Do you want to save changes to \"{}\"?", name);
|
|
|
|
|
|
|
|
|
|
|
|
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
|
|
|
|
|
button(text(label).size(13).color(TEXT_COL))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered | button::Status::Pressed => hov,
|
|
|
|
|
|
_ => base,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT_COL,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BORDER_COL,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
2026-05-01 11:31:11 +03:00
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
|
snap: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([6, 18])
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
container(
|
|
|
|
|
|
column![
|
|
|
|
|
|
text(body_text).size(13).color(TEXT_COL),
|
|
|
|
|
|
iced::widget::Space::new().height(20),
|
|
|
|
|
|
row![
|
2026-05-12 10:36:15 +03:00
|
|
|
|
btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER),
|
2026-05-01 11:31:11 +03:00
|
|
|
|
iced::widget::Space::new().width(8),
|
|
|
|
|
|
btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV),
|
|
|
|
|
|
iced::widget::Space::new().width(8),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV),
|
2026-05-01 11:31:11 +03:00
|
|
|
|
],
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(move |_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.center(Fill)
|
|
|
|
|
|
.padding([24, 28])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Start / Welcome page ──────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Renders in place of the model-space viewport when the active tab is the
|
|
|
|
|
|
// fixed Start tab (`DocumentTab::is_start`). English-only by design — this
|
|
|
|
|
|
// is the public welcome screen and stays consistent across locales.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The page picks up the application icon's red-brown (#B03020) as a tint so
|
2026-05-23 17:16:15 +03:00
|
|
|
|
// it visually belongs to OpenCADStudio without overpowering the dark workspace.
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
|
|
|
|
|
|
const BRAND: Color = Color { r: 0.690, g: 0.188, b: 0.125, a: 1.0 }; // #B03020
|
|
|
|
|
|
const BRAND_DARK: Color = Color { r: 0.45, g: 0.12, b: 0.08, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn start_page_view<'a>() -> Element<'a, Message> {
|
|
|
|
|
|
const TEXT: Color = Color { r: 0.94, g: 0.93, b: 0.92, a: 1.0 };
|
|
|
|
|
|
const MUTED: Color = Color { r: 0.62, g: 0.62, b: 0.62, a: 1.0 };
|
|
|
|
|
|
const CARD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.13, a: 1.0 };
|
|
|
|
|
|
const CARD_BORDER: Color = Color { r: 0.20, g: 0.20, b: 0.22, a: 1.0 };
|
|
|
|
|
|
|
2026-05-23 17:16:15 +03:00
|
|
|
|
// Brand-tinted "Welcome to" — the "OpenCADStudio" word takes the accent colour
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
// (Thunderbird-style coloured headline split).
|
|
|
|
|
|
let headline = row![
|
|
|
|
|
|
text("Welcome to ").size(40).color(TEXT),
|
2026-05-23 17:16:15 +03:00
|
|
|
|
text("Open CAD Studio").size(40).color(BRAND),
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
let subtitle = text(
|
2026-05-23 17:16:15 +03:00
|
|
|
|
"Open CAD Studio is an open-source CAD viewer and editor — a gift from contributors like you. \
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
Open a DWG/DXF file, start a new drawing, or help shape what comes next.",
|
|
|
|
|
|
)
|
|
|
|
|
|
.size(13)
|
|
|
|
|
|
.color(MUTED);
|
|
|
|
|
|
|
|
|
|
|
|
// Plain outlined button (Open / New / Help / Contribute).
|
|
|
|
|
|
let outline_btn = |label: &'static str, msg: Message| {
|
|
|
|
|
|
button(text(label).size(14).color(TEXT))
|
|
|
|
|
|
.on_press(msg)
|
|
|
|
|
|
.padding([10, 22])
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 },
|
|
|
|
|
|
_ => Color { r: 0.13, g: 0.13, b: 0.15, a: 1.0 },
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color { r: 0.30, g: 0.30, b: 0.33, a: 1.0 },
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Donate — the prominent call-to-action. Solid brand fill, white text.
|
|
|
|
|
|
let donate_btn = {
|
|
|
|
|
|
button(
|
|
|
|
|
|
row![
|
|
|
|
|
|
text("♥ ").size(15).color(Color::WHITE),
|
|
|
|
|
|
text("Donate").size(14).color(Color::WHITE),
|
|
|
|
|
|
]
|
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::RibbonToolClick {
|
|
|
|
|
|
tool_id: "DONATE".to_string(),
|
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("DONATE".to_string()),
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([12, 28])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => BRAND_DARK,
|
|
|
|
|
|
_ => BRAND,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: Color::WHITE,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: BRAND_DARK,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 6.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
shadow: iced::Shadow {
|
|
|
|
|
|
color: Color { r: 0.0, g: 0.0, b: 0.0, a: 0.4 },
|
|
|
|
|
|
offset: iced::Vector::new(0.0, 2.0),
|
|
|
|
|
|
blur_radius: 6.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let primary_row = row![
|
|
|
|
|
|
outline_btn("New Drawing", Message::TabNew),
|
|
|
|
|
|
outline_btn("Open File…", Message::OpenFile),
|
|
|
|
|
|
donate_btn,
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(12)
|
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
let secondary_row = row![
|
|
|
|
|
|
outline_btn(
|
|
|
|
|
|
"Contribute",
|
|
|
|
|
|
Message::RibbonToolClick {
|
|
|
|
|
|
tool_id: "REPORT".to_string(),
|
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("REPORT".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
outline_btn(
|
|
|
|
|
|
"Release Notes",
|
|
|
|
|
|
Message::RibbonToolClick {
|
|
|
|
|
|
tool_id: "CHANGELOG".to_string(),
|
|
|
|
|
|
event: crate::modules::ModuleEvent::Command("CHANGELOG".to_string()),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
outline_btn("About", Message::AboutOpen),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(12)
|
|
|
|
|
|
.align_y(iced::Center);
|
|
|
|
|
|
|
|
|
|
|
|
let card_section = |heading: &'static str, sub: &'static str, body: &'static str| {
|
|
|
|
|
|
container(
|
|
|
|
|
|
column![
|
|
|
|
|
|
text(heading).size(20).color(TEXT),
|
|
|
|
|
|
text(sub).size(13).color(MUTED),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(10.0)),
|
|
|
|
|
|
text(body).size(12).color(MUTED),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(6),
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(20)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(CARD_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: CARD_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 8.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let cards = row![
|
|
|
|
|
|
card_section(
|
|
|
|
|
|
"Free and open source",
|
2026-05-23 17:16:15 +03:00
|
|
|
|
"Open CAD Studio belongs to its users.",
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
"Source is openly developed and distributed under a permissive licence. \
|
2026-05-23 17:16:15 +03:00
|
|
|
|
You are free to use, study, modify, and share Open CAD Studio.",
|
feat(app): add fixed Start tab with welcome page and recent documents
A pinned "Start" tab now lives at index 0. It cannot be closed, opens
on launch, and replaces the model-space viewport with a welcome page —
similar to Thunderbird's first-run screen.
- Boot opens with only the Start tab; Drawing1 is no longer auto-spawned.
- The Properties left panel becomes a Recent Documents rail on the
Start tab. Recents persist to disk
(%APPDATA%/H7CAD/recent.txt on Windows, $XDG_CONFIG_HOME/H7CAD/recent.txt
on Linux, ~/Library/Application Support/H7CAD/recent.txt on macOS), up
to 20 entries, saved on every open and rehydrated at boot.
Each row opens or removes the file in place.
- The Support ribbon group (Donate / Report / About / Changelog) is
retired; the same actions surface on the welcome page instead. The
Donate button is the primary call-to-action — solid #B03020 brand
fill with a soft brand-tinted ambient glow halo behind the action row
(Thunderbird-style coloured glow against the neutral page).
- Drawing-only overlays (mouse-area, viewcube, navbar, dynamic input,
selection markers) are suppressed on the Start tab so its clicks
don't trigger CAD commands. OpenFile's "current_is_empty" check
refuses to replace the Start tab, and TabClose ignores requests on
it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:19:31 +03:00
|
|
|
|
),
|
|
|
|
|
|
card_section(
|
|
|
|
|
|
"Community driven",
|
|
|
|
|
|
"Be part of the story.",
|
|
|
|
|
|
"Anyone can contribute — translations, bug reports, feature ideas, code, \
|
|
|
|
|
|
or simply spreading the word. Your involvement shapes the project.",
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(16);
|
|
|
|
|
|
|
|
|
|
|
|
// Buttons sit on a transparent container with a large, brand-tinted
|
|
|
|
|
|
// ambient shadow (offset = 0, big blur) — produces a soft halo behind
|
|
|
|
|
|
// the action row, matching the Thunderbird coloured-glow look against
|
|
|
|
|
|
// the dark page.
|
|
|
|
|
|
let primary_glow = container(primary_row)
|
|
|
|
|
|
.padding(iced::Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 8.0 })
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(Color::TRANSPARENT)),
|
|
|
|
|
|
shadow: iced::Shadow {
|
|
|
|
|
|
color: Color { r: BRAND.r, g: BRAND.g, b: BRAND.b, a: 0.45 },
|
|
|
|
|
|
offset: iced::Vector::ZERO,
|
|
|
|
|
|
blur_radius: 80.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let content = column![
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(28.0)),
|
|
|
|
|
|
container(headline).center_x(Fill),
|
|
|
|
|
|
container(subtitle).center_x(Fill).padding([10, 60]),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(22.0)),
|
|
|
|
|
|
container(primary_glow).center_x(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(10.0)),
|
|
|
|
|
|
container(secondary_row).center_x(Fill),
|
|
|
|
|
|
Space::new().height(iced::Length::Fixed(40.0)),
|
|
|
|
|
|
cards,
|
|
|
|
|
|
Space::new().height(Fill),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill);
|
|
|
|
|
|
|
|
|
|
|
|
// Page background reverts to plain dark — the glow alone provides the
|
|
|
|
|
|
// brand colour cue, the rest of the page stays neutral so it reads as
|
|
|
|
|
|
// "workspace area" not "advertising banner".
|
|
|
|
|
|
const PAGE_BG: Color = Color { r: 0.08, g: 0.08, b: 0.085, a: 1.0 };
|
|
|
|
|
|
container(content)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PAGE_BG)),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding(iced::Padding { top: 40.0, right: 60.0, bottom: 40.0, left: 60.0 })
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Recent Documents panel (Start tab left rail) ──────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Slots into the same `row![properties_el, viewport_stack]` position the
|
|
|
|
|
|
// Properties panel normally occupies, but only when the active tab is the
|
|
|
|
|
|
// Start tab. The list is restored from disk at boot and re-saved on every
|
|
|
|
|
|
// open — entries persist across sessions.
|
|
|
|
|
|
pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Element<'a, Message> {
|
|
|
|
|
|
const PANEL_BG: Color = Color { r: 0.10, g: 0.10, b: 0.11, a: 1.0 };
|
|
|
|
|
|
const PANEL_BORDER: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 };
|
|
|
|
|
|
const ITEM_HOVER: Color = Color { r: 0.16, g: 0.16, b: 0.18, a: 1.0 };
|
|
|
|
|
|
const TEXT: Color = Color { r: 0.92, g: 0.91, b: 0.90, a: 1.0 };
|
|
|
|
|
|
const MUTED: Color = Color { r: 0.60, g: 0.60, b: 0.62, a: 1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
let header = container(text("Recent Documents").size(11).color(MUTED))
|
|
|
|
|
|
.padding(iced::Padding { top: 12.0, right: 14.0, bottom: 8.0, left: 14.0 });
|
|
|
|
|
|
|
|
|
|
|
|
let body: Element<'a, Message> = if recents.is_empty() {
|
|
|
|
|
|
container(
|
|
|
|
|
|
text("Files you open will show up here.")
|
|
|
|
|
|
.size(11)
|
|
|
|
|
|
.color(MUTED),
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding([10, 14])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut col = column![].spacing(0);
|
|
|
|
|
|
for path in recents {
|
|
|
|
|
|
let name = path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
|
|
|
|
|
let dir = path
|
|
|
|
|
|
.parent()
|
|
|
|
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
|
|
let path_for_open = path.clone();
|
|
|
|
|
|
let open_btn = button(
|
|
|
|
|
|
column![
|
|
|
|
|
|
text(name).size(12).color(TEXT),
|
|
|
|
|
|
text(dir).size(10).color(MUTED),
|
|
|
|
|
|
]
|
|
|
|
|
|
.spacing(2),
|
|
|
|
|
|
)
|
|
|
|
|
|
.on_press(Message::OpenRecent(path_for_open))
|
|
|
|
|
|
.padding([6, 12])
|
|
|
|
|
|
.width(Fill)
|
|
|
|
|
|
.style(move |_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => ITEM_HOVER,
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: TEXT,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let path_for_remove = path.clone();
|
|
|
|
|
|
let remove_btn = button(text("×").size(12).color(MUTED))
|
|
|
|
|
|
.on_press(Message::RecentRemove(path_for_remove))
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.style(|_: &Theme, status| button::Style {
|
|
|
|
|
|
background: Some(Background::Color(match status {
|
|
|
|
|
|
button::Status::Hovered => Color { r: 0.45, g: 0.15, b: 0.15, a: 1.0 },
|
|
|
|
|
|
_ => Color::TRANSPARENT,
|
|
|
|
|
|
})),
|
|
|
|
|
|
text_color: MUTED,
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: Color::TRANSPARENT,
|
|
|
|
|
|
width: 0.0,
|
|
|
|
|
|
radius: 3.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
col = col.push(
|
|
|
|
|
|
row![open_btn, remove_btn]
|
|
|
|
|
|
.spacing(0)
|
|
|
|
|
|
.align_y(iced::Center),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
iced::widget::scrollable(col).into()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
container(column![header, body])
|
|
|
|
|
|
.width(iced::Length::Fixed(280.0))
|
|
|
|
|
|
.height(Fill)
|
|
|
|
|
|
.style(|_: &Theme| container::Style {
|
|
|
|
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
|
|
|
|
border: Border {
|
|
|
|
|
|
color: PANEL_BORDER,
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 0.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
|
2026-05-27 18:05:09 +03:00
|
|
|
|
// ── Render-mode picker ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// The 7-way visual-style `pick_list`, styled as a dark overlay chip.
|
|
|
|
|
|
/// Shared by the model-space viewport (top-left) and each active
|
|
|
|
|
|
/// paper-space viewport. Emits `SetRenderMode`, which the update loop
|
|
|
|
|
|
/// routes to the active viewport entity or the model-layout tab.
|
|
|
|
|
|
fn render_mode_picker<'a>(
|
|
|
|
|
|
current: acadrust::entities::ViewportRenderMode,
|
|
|
|
|
|
) -> Element<'a, Message> {
|
|
|
|
|
|
use acadrust::entities::ViewportRenderMode as M;
|
|
|
|
|
|
let render_modes: Vec<RenderModeChoice> = vec![
|
|
|
|
|
|
RenderModeChoice(M::Wireframe2D),
|
|
|
|
|
|
RenderModeChoice(M::Wireframe3D),
|
|
|
|
|
|
RenderModeChoice(M::HiddenLine),
|
|
|
|
|
|
RenderModeChoice(M::FlatShaded),
|
|
|
|
|
|
RenderModeChoice(M::GouraudShaded),
|
|
|
|
|
|
RenderModeChoice(M::FlatShadedWithEdges),
|
|
|
|
|
|
RenderModeChoice(M::GouraudShadedWithEdges),
|
|
|
|
|
|
];
|
|
|
|
|
|
container(
|
|
|
|
|
|
iced::widget::pick_list(render_modes, Some(RenderModeChoice(current)), |c| {
|
|
|
|
|
|
Message::SetRenderMode(c.0)
|
|
|
|
|
|
})
|
|
|
|
|
|
.text_size(11),
|
|
|
|
|
|
)
|
|
|
|
|
|
.style(|_: &Theme| iced::widget::container::Style {
|
|
|
|
|
|
background: Some(iced::Background::Color(Color {
|
|
|
|
|
|
r: 0.10,
|
|
|
|
|
|
g: 0.10,
|
|
|
|
|
|
b: 0.10,
|
|
|
|
|
|
a: 0.75,
|
|
|
|
|
|
})),
|
|
|
|
|
|
border: iced::Border {
|
|
|
|
|
|
color: Color {
|
|
|
|
|
|
r: 0.35,
|
|
|
|
|
|
g: 0.35,
|
|
|
|
|
|
b: 0.35,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
width: 1.0,
|
|
|
|
|
|
radius: 4.0.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
text_color: Some(Color {
|
|
|
|
|
|
r: 0.85,
|
|
|
|
|
|
g: 0.85,
|
|
|
|
|
|
b: 0.85,
|
|
|
|
|
|
a: 1.0,
|
|
|
|
|
|
}),
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
})
|
|
|
|
|
|
.padding([4, 8])
|
|
|
|
|
|
.into()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
// ── Dynamic-input field formatting ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// Short prefix shown before a dynamic-input box's value.
|
|
|
|
|
|
fn dyn_component_label(c: DynComponent) -> String {
|
|
|
|
|
|
match c {
|
|
|
|
|
|
DynComponent::X => "X".into(),
|
|
|
|
|
|
DynComponent::Y => "Y".into(),
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
|
DynComponent::Z => "Z".into(),
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
DynComponent::Distance => "d".into(),
|
|
|
|
|
|
DynComponent::Angle => "<".into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The string shown inside a dynamic-input box: the typed buffer when the
|
|
|
|
|
|
/// field is locked, otherwise the live value derived from the cursor
|
|
|
|
|
|
/// world position (and the base point for polar quantities).
|
|
|
|
|
|
fn dyn_component_value(
|
|
|
|
|
|
f: &DynFieldEntry,
|
|
|
|
|
|
w: glam::Vec3,
|
|
|
|
|
|
base: Option<glam::Vec3>,
|
|
|
|
|
|
) -> String {
|
|
|
|
|
|
if let Some(b) = &f.buffer {
|
|
|
|
|
|
return b.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
let b = base.unwrap_or(glam::Vec3::ZERO);
|
|
|
|
|
|
let dx = (w.x - b.x) as f64;
|
|
|
|
|
|
let dy = (w.y - b.y) as f64;
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
|
// When a base point exists (DYN-on after the first pick) the cartesian
|
|
|
|
|
|
// fields show relative deltas — matching the typed-value convention
|
|
|
|
|
|
// in `dyn_resolve_point` so the live preview and the committed
|
|
|
|
|
|
// coordinate use the same frame. See #35.
|
|
|
|
|
|
let has_base = base.is_some();
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
match f.component {
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
|
DynComponent::X if has_base => format!("{:.4}", dx),
|
|
|
|
|
|
DynComponent::Y if has_base => format!("{:.4}", dy),
|
|
|
|
|
|
DynComponent::Z if has_base => "0.0000".to_string(),
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
DynComponent::X => format!("{:.4}", w.x),
|
|
|
|
|
|
DynComponent::Y => format!("{:.4}", w.y),
|
fix(dyn-input): support cartesian coords via "," (#35)
Typing \`,\` while DYN was on appended the character to the focused
field's buffer (treated as a European decimal point), so users
couldn't switch to cartesian coordinates mid-command. The dynamic
input was stuck in polar \`[Distance, Angle]\` whenever a base point
existed.
\`,\` now acts as the coordinate separator (matching the AutoCAD
convention): pressing it locks the current field's buffer, then
either advances within the existing cartesian set or reshapes a
polar configuration into cartesian:
- \`[Distance, Angle]\` + \`,\` on the first field → \`[X(buf), Y]\`
- \`[X, Y]\` + \`,\` on Y → \`[X, Y, Z]\`
- otherwise advances to the next field (Tab-equivalent)
\`sync_dyn_fields\` accepts both polar and cartesian shapes as valid
\`Point\` configurations so a mouse-move after the reshape doesn't
revert to polar. \`dyn_resolve_point\` and the overlay's live-value
formatter treat cartesian inputs as RELATIVE to the base point when
one exists — matching the DYN-on relative-coords convention from
#26. \`DynComponent::Z\` plus its label / value paths fill out the
3-D variant.
Closes #35
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:46:18 +03:00
|
|
|
|
DynComponent::Z => format!("{:.4}", b.z),
|
feat(dyn-input): TAB-editable distance/angle/coordinate fields
Dinamik giriş artık cursor yanında düzenlenebilir kutular gösteriyor.
TAB ile kutular arası geçilir, bir kutuya yazınca o kutu kilitlenir
(değer sabitlenir), diğerleri cursor'ı takip etmeye devam eder, Enter
ile çözülen nokta komuta point-pick olarak iletilir.
- DocumentTab.dyn_fields / dyn_active state; DynComponent + DynFieldEntry
- sync_dyn_fields komutun dyn_field() + base noktasına göre kutu kümesini
türetir: nokta+base → [d, açı], nokta+base yok → [X, Y], radius → [d],
açı → [açı]
- klavye yönlendirme: rakam/./-/+ aktif kutuya, TAB sonraki kutuya,
Enter try_dyn_commit, Backspace kilidi açar
- dyn_capturing iken command-line text alanı on_input'suz bırakılıyor
ki focus'u ve tuşları çalmasın; girişler global key aboneliğinden
geliyor (numpad dahil text alanı üzerinden yakalanıyor)
- çoklu-kutu overlay render: aktif kutu vurgulu, kilitli kutu farklı
kenar rengi
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:08:57 +03:00
|
|
|
|
DynComponent::Distance => format!("{:.4}", (dx * dx + dy * dy).sqrt()),
|
|
|
|
|
|
DynComponent::Angle => format!("{:.1}", dy.atan2(dx).to_degrees().rem_euclid(360.0)),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|