mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
* Block Editor: BEDIT, BSAVE, BSAVEAS and BCLOSE for static blocks BEDIT opens a block definition in the drawing's own tab with its base point at the origin, and the block's objects are edited with the ordinary commands. BSAVE writes them back into the definition, so every reference in the drawing updates (one undo step in the drawing); BSAVEAS saves under a new name; BCLOSE goes back to the drawing, asking whether to save unsaved changes. - The drawing waits untouched while the editor is open (blockedit.rs): its undo history, selection and views come back on BCLOSE. Objects left unchanged are saved back exactly, so no coordinate drifts. - Files save the drawing outside the editor; autosave keeps the editor's unsaved changes in the block; the close/quit prompt's Save puts them into the block first. - Right-click a selected block reference: Block Editor. In the editor the canvas is tinted, the base point is marked, and a bar offers Save Block, Save Block As and Close Block Editor (also in the right-click menu). The Block tool set's Block Editor button is now live. BLOCKEDITOR sysvar. - Layout and paper-space commands are refused while the editor is open; a block can't be saved to contain a reference to itself. - Dynamic blocks: their data is kept; the editor says it can't edit their parameters yet. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Edit Block Definition dialog; BEDIT can start a new block BEDIT typed or started from a menu or the Tool Sets with no block reference selected opens the Edit Block Definition dialog: the drawing's blocks sorted by name, a preview and the description. OK opens the Block Editor on the block named; a name no block has yet starts a new block, which BSAVE creates (`bedit {name, create: true}`). A selected block reference still opens at once, and JSON calls never open the dialog. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Edit Block Definition: centred window, list box; gaps.md F3 The dialog opens centred on the window, the blocks are a full-width list in a list box of fixed height, and gaps.md F3 mentions the dialog. Co-Authored-By: Claude Opus 5.5 <[email protected]> * BEDIT: find dynamic block data on the block's BLOCK_RECORD A dynamic block's ACAD_ENHANCEDBLOCK dictionary hangs off its BLOCK_RECORD table entry, not the BLOCK entity, so the "parameters are kept" note never showed for AutoCAD files. Regression test with the data laid out as an AutoCAD DXF has it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * BEDIT: a dynamic block reference opens its dynamic block A dynamic block reference with values of its own shows an anonymous block (`*U12`), so the right-click Block Editor (and BEDIT on a picked or selected reference) refused it and asked for a name. The anonymous block's record names its dynamic block in AcDbBlockRepBTag xdata; BEDIT now opens that, as AutoCAD does. Regression test. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Block Editor on the current drawing; opening a drawing with authoring elements offers it A dynamic block's own drawing (model space with authoring elements: its block record's ACAD_ENHANCEDBLOCK) opened from the UI asks, as AutoCAD does, "<file> contains authoring elements. Open in Block Editor?" Yes / No / Cancel. Yes opens the Block Editor on <Current Drawing>: the drawing's own objects, edited where they are; BSAVE keeps them, BCLOSE without saving puts the drawing back. <Current Drawing> heads the Edit Block Definition list. BSAVEAS is not available there. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Cody Vogt <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
566 lines
25 KiB
Rust
566 lines
25 KiB
Rust
//! Dialogs: Drafting Settings, Drawing Units, About, command reference, blocks; dispatches the
|
|
//! Layer Properties Manager ([`crate::layers`]), Quick Select ([`crate::quick`]), Parameters
|
|
//! Manager ([`crate::parametric`]) and the style and settings managers ([`crate::managers`]).
|
|
|
|
use egui::{RichText, vec2};
|
|
use serde_json::json;
|
|
|
|
use crate::CadApp;
|
|
use crate::theme::Tokens;
|
|
|
|
pub fn show(app: &mut CadApp, ctx: &egui::Context) {
|
|
mtext_editor(app, ctx);
|
|
crate::quick::quick_properties(app, ctx);
|
|
let Some(d) = app.ui.dialog.clone() else { return };
|
|
let mut open = true;
|
|
match d.as_str() {
|
|
"spacemouse" => crate::spacemouse::dialog(app, ctx, &mut open),
|
|
"language" => language(app, ctx, &mut open),
|
|
"layers" => crate::layers::dialog(app, ctx, &mut open),
|
|
"qselect" => crate::quick::qselect_dialog(app, ctx, &mut open),
|
|
"parameters" => crate::parametric::parameters_dialog(app, ctx, &mut open),
|
|
"dsettings" => dsettings(app, ctx, &mut open),
|
|
"units" => units(app, ctx, &mut open),
|
|
"hatch" => crate::hatchdlg::dialog(app, ctx, &mut open),
|
|
"about" => about(ctx, &mut open),
|
|
"preview" => crate::exchange::dialog(app, ctx, &mut open),
|
|
"commands" => commands(app, ctx, &mut open),
|
|
"blocks" => blocks(app, ctx, &mut open),
|
|
"bedit" => crate::bedit::dialog(app, ctx, &mut open),
|
|
"authoring" => crate::bedit::authoring_dialog(app, ctx, &mut open),
|
|
"stylesmanager" => crate::plotstyles::dialog(app, ctx, &mut open),
|
|
o if o == "options" || o.starts_with("options:") => crate::options::dialog(app, ctx, o, &mut open),
|
|
other => crate::managers::dialog(app, ctx, other, &mut open),
|
|
}
|
|
// A dialog that closed by opening another (Plot Preview back to Plot) keeps the new one.
|
|
if !open && app.ui.dialog.as_deref() == Some(d.as_str()) {
|
|
app.ui.dialog = None;
|
|
}
|
|
}
|
|
|
|
fn dsettings(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
|
let settings = app.session.drafting_settings();
|
|
let (mut snapunit, mut gridunit) = (settings.snapunit, settings.gridunit);
|
|
egui::Window::new(crate::tl!("Drafting Settings")).id(egui::Id::new("Drafting Settings")).open(open).resizable(false).show(ctx, |ui| {
|
|
let s = &mut app.session.settings;
|
|
ui.heading(crate::tl!("Snap and Grid"));
|
|
ui.checkbox(&mut s.snapmode, crate::tl!("Snap On (F9)"));
|
|
// SNAPSTYL: the isometric snap and grid (ISODRAFT).
|
|
ui.horizontal(|ui| {
|
|
ui.radio_value(&mut s.isodraft, false, crate::tl!("Rectangular snap"));
|
|
ui.radio_value(&mut s.isodraft, true, crate::tl!("Isometric snap"));
|
|
});
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Snap X spacing"));
|
|
ui.add(egui::DragValue::new(&mut snapunit.x).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));
|
|
ui.label(crate::tl!("Y"));
|
|
ui.add(egui::DragValue::new(&mut snapunit.y).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));
|
|
});
|
|
// SNAPTYPE and POLARDIST.
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Snap type"));
|
|
ui.radio_value(&mut s.polarsnap, false, crate::tl!("Grid snap"));
|
|
ui.radio_value(&mut s.polarsnap, true, crate::tl!("PolarSnap"));
|
|
});
|
|
ui.add_enabled_ui(s.polarsnap, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Polar distance"));
|
|
ui.add(egui::DragValue::new(&mut s.polardist).speed(0.05).range(0.0..=1e6));
|
|
});
|
|
});
|
|
ui.checkbox(&mut s.gridmode, crate::tl!("Grid On (F7)"));
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Grid spacing"));
|
|
ui.add(egui::DragValue::new(&mut gridunit.x).speed(0.05).range(0.0..=1e6).clamp_existing_to_range(false));
|
|
ui.label(crate::tl!("Major line every"));
|
|
ui.add(egui::DragValue::new(&mut s.gridmajor).range(1..=100));
|
|
});
|
|
ui.separator();
|
|
ui.heading(crate::tl!("Polar Tracking"));
|
|
ui.checkbox(&mut s.polarmode, crate::tl!("Polar Tracking On (F10)"));
|
|
let mut deg = s.polarang.to_degrees();
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Increment angle"));
|
|
egui::ComboBox::from_id_salt("polarang").selected_text(format!("{deg}")).show_ui(ui, |ui| {
|
|
for a in [90.0, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0] {
|
|
ui.selectable_value(&mut deg, a, format!("{a}"));
|
|
}
|
|
});
|
|
});
|
|
s.polarang = deg.to_radians();
|
|
tracking_options(ui, s);
|
|
ui.separator();
|
|
ui.heading(crate::tl!("Object Snap"));
|
|
let mut on = s.osmode & cadcraft_engine::snap::mode::OFF == 0;
|
|
if ui.checkbox(&mut on, crate::tl!("Object Snap On (F3)")).changed() {
|
|
if on {
|
|
s.osmode &= !cadcraft_engine::snap::mode::OFF;
|
|
} else {
|
|
s.osmode |= cadcraft_engine::snap::mode::OFF;
|
|
}
|
|
}
|
|
egui::Grid::new("osnap_grid").num_columns(2).show(ui, |ui| {
|
|
for (i, (bit, name)) in cadcraft_engine::snap::mode::ALL.iter().enumerate() {
|
|
let mut v = s.osmode & bit != 0;
|
|
if ui.checkbox(&mut v, crate::i18n::t(name)).changed() {
|
|
if v {
|
|
s.osmode |= bit;
|
|
} else {
|
|
s.osmode &= !bit;
|
|
}
|
|
}
|
|
if i % 2 == 1 {
|
|
ui.end_row();
|
|
}
|
|
}
|
|
});
|
|
ui.separator();
|
|
ui.checkbox(&mut s.dynmode, crate::tl!("Enable Dynamic Input (F12)"));
|
|
// Pointer input format for second and next points (DYNPIFORMAT, DYNPICOORDS).
|
|
ui.indent("dynpi", |ui| {
|
|
ui.add_enabled_ui(s.dynmode, |ui| {
|
|
ui.label(crate::tl!("Second and next points:"));
|
|
ui.horizontal(|ui| {
|
|
ui.radio_value(&mut s.dynpi_cartesian, false, crate::tl!("Polar (distance < angle)"));
|
|
ui.radio_value(&mut s.dynpi_cartesian, true, crate::tl!("Cartesian (x, y)"));
|
|
});
|
|
ui.horizontal(|ui| {
|
|
ui.radio_value(&mut s.dynpi_absolute, false, crate::tl!("Relative"));
|
|
ui.radio_value(&mut s.dynpi_absolute, true, crate::tl!("Absolute"));
|
|
});
|
|
// DYNPROMPT: the prompt and its options (Down Arrow) beside the cursor.
|
|
ui.checkbox(&mut s.dynprompt, crate::tl!("Show command prompting near the crosshairs"));
|
|
});
|
|
});
|
|
ui.checkbox(&mut s.orthomode, crate::tl!("Ortho (F8)"));
|
|
});
|
|
// Drawing edits go through the same command as the CLI, including dirty state and undo.
|
|
for (name, before, after) in [("SNAPUNIT", settings.snapunit, snapunit), ("GRIDUNIT", settings.gridunit, gridunit)] {
|
|
if before != after {
|
|
let _ = app.run("setvar", json!({ "name": name, "value": [after.x, after.y] }));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The polar and object snap tracking options of Drafting Settings: additional angles
|
|
/// (POLARMODE bit 4, POLARADDANG), tracking on every polar angle (bit 2) and angles relative to
|
|
/// the last segment (bit 1).
|
|
fn tracking_options(ui: &mut egui::Ui, s: &mut cadcraft_engine::Settings) {
|
|
use cadcraft_engine::snap::tracking::polarmode;
|
|
/// A radio button for POLARMODE `bit` being `on`.
|
|
fn flag(ui: &mut egui::Ui, flags: &mut u32, bit: u32, on: bool, label: &str) {
|
|
if ui.radio((*flags & bit != 0) == on, label).clicked() {
|
|
*flags = if on { *flags | bit } else { *flags & !bit };
|
|
}
|
|
}
|
|
let mut additional = s.polar_flags & polarmode::ADDITIONAL != 0;
|
|
ui.horizontal(|ui| {
|
|
if ui.checkbox(&mut additional, crate::tl!("Additional angles")).changed() {
|
|
s.polar_flags ^= polarmode::ADDITIONAL;
|
|
}
|
|
// The list as typed (degrees separated by `;`), kept while it doesn't parse yet.
|
|
let id = ui.id().with("polaraddang");
|
|
let shown = cadcraft_engine::sysvars::format_angle_list(&s.polaraddang);
|
|
let mut text = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| shown.clone());
|
|
let resp = ui.add_enabled(additional, egui::TextEdit::singleline(&mut text).desired_width(140.0));
|
|
if resp.changed()
|
|
&& let Some(angles) = cadcraft_engine::sysvars::parse_angle_list(&text)
|
|
{
|
|
s.polaraddang = angles;
|
|
}
|
|
if resp.has_focus() {
|
|
ui.data_mut(|d| d.insert_temp(id, text));
|
|
} else {
|
|
ui.data_mut(|d| d.remove::<String>(id));
|
|
}
|
|
});
|
|
ui.label(crate::tl!("Object Snap Tracking Settings"));
|
|
ui.horizontal(|ui| {
|
|
flag(ui, &mut s.polar_flags, polarmode::TRACK_POLAR, false, crate::tl!("Track orthogonally only"));
|
|
flag(ui, &mut s.polar_flags, polarmode::TRACK_POLAR, true, crate::tl!("Track using all polar angle settings"));
|
|
});
|
|
ui.label(crate::tl!("Polar angle measurement"));
|
|
ui.horizontal(|ui| {
|
|
flag(ui, &mut s.polar_flags, polarmode::RELATIVE, false, crate::tl!("Absolute"));
|
|
flag(ui, &mut s.polar_flags, polarmode::RELATIVE, true, crate::tl!("Relative to last segment"));
|
|
});
|
|
}
|
|
|
|
/// LUNITS values in the order the Type list shows them.
|
|
const LENGTH_TYPES: [(i64, &str); 5] = [(4, "Architectural"), (2, "Decimal"), (3, "Engineering"), (5, "Fractional"), (1, "Scientific")];
|
|
/// AUNITS values.
|
|
const ANGLE_TYPES: [(i64, &str); 5] = [(0, "Decimal Degrees"), (1, "Deg/Min/Sec"), (2, "Grads"), (3, "Radians"), (4, "Surveyor's Units")];
|
|
/// INSUNITS names, indexed by value (DXF Reference, header group code 70).
|
|
const INSERTION_UNITS: [&str; 25] = [
|
|
"Unitless",
|
|
"Inches",
|
|
"Feet",
|
|
"Miles",
|
|
"Millimeters",
|
|
"Centimeters",
|
|
"Meters",
|
|
"Kilometers",
|
|
"Microinches",
|
|
"Mils",
|
|
"Yards",
|
|
"Angstroms",
|
|
"Nanometers",
|
|
"Microns",
|
|
"Decimeters",
|
|
"Decameters",
|
|
"Hectometers",
|
|
"Gigameters",
|
|
"Astronomical Units",
|
|
"Light Years",
|
|
"Parsecs",
|
|
"US Survey Feet",
|
|
"US Survey Inch",
|
|
"US Survey Yard",
|
|
"US Survey Mile",
|
|
];
|
|
|
|
/// A combo box over `(value, label)` options.
|
|
fn pick(ui: &mut egui::Ui, salt: &str, value: &mut i64, options: &[(i64, String)]) {
|
|
let current = options.iter().find(|o| o.0 == *value).map(|o| o.1.clone()).unwrap_or_default();
|
|
egui::ComboBox::from_id_salt(salt).selected_text(current).width(180.0).show_ui(ui, |ui| {
|
|
for (k, label) in options {
|
|
ui.selectable_value(value, *k, label);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Drawing Units (UNITS): edits a copy of LUNITS/LUPREC/AUNITS/AUPREC/INSUNITS and applies it
|
|
/// through the `units` command on OK.
|
|
fn units(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
|
use cadcraft_engine::units::{format_angle, format_distance};
|
|
let id = egui::Id::new("units_dialog_values");
|
|
let mut v = ctx.data_mut(|d| d.get_temp::<[i64; 5]>(id)).unwrap_or_else(|| {
|
|
app.session
|
|
.doc()
|
|
.map(|d| {
|
|
let h = &d.header;
|
|
[h.i64("LUNITS", 2), h.i64("LUPREC", 4), h.i64("AUNITS", 0), h.i64("AUPREC", 0), h.i64("INSUNITS", 1)]
|
|
})
|
|
.unwrap_or([2, 4, 0, 0, 1])
|
|
});
|
|
let (mut apply, mut cancel) = (false, false);
|
|
egui::Window::new("Drawing Units").open(open).resizable(false).collapsible(false).show(ctx, |ui| {
|
|
let [lu, lp, au, ap, ins] = &mut v;
|
|
let types = |t: &[(i64, &str)]| t.iter().map(|(k, l)| (*k, l.to_string())).collect::<Vec<_>>();
|
|
egui::Grid::new("units_grid").num_columns(2).spacing(vec2(12.0, 6.0)).show(ui, |ui| {
|
|
ui.strong("Length");
|
|
ui.end_row();
|
|
ui.label("Type");
|
|
pick(ui, "units_lunits", lu, &types(&LENGTH_TYPES));
|
|
ui.end_row();
|
|
ui.label("Precision");
|
|
pick(ui, "units_luprec", lp, &(0..=8).map(|p| (p, format_distance(1.5, *lu, p))).collect::<Vec<_>>());
|
|
ui.end_row();
|
|
ui.strong("Angle");
|
|
ui.end_row();
|
|
ui.label("Type");
|
|
pick(ui, "units_aunits", au, &types(&ANGLE_TYPES));
|
|
ui.end_row();
|
|
ui.label("Precision");
|
|
pick(ui, "units_auprec", ap, &(0..=8).map(|p| (p, format_angle(45f64.to_radians(), *au, p))).collect::<Vec<_>>());
|
|
ui.end_row();
|
|
ui.strong("Insertion scale");
|
|
ui.end_row();
|
|
ui.label("Units to scale inserted content");
|
|
let names = INSERTION_UNITS.iter().enumerate().map(|(k, l)| (k as i64, l.to_string())).collect::<Vec<_>>();
|
|
pick(ui, "units_insunits", ins, &names);
|
|
ui.end_row();
|
|
ui.strong("Sample output");
|
|
ui.end_row();
|
|
ui.label("");
|
|
ui.label(format!("{},{},{}", format_distance(1.5, *lu, *lp), format_distance(2.0039, *lu, *lp), format_distance(0.0, *lu, *lp)));
|
|
ui.end_row();
|
|
ui.label("");
|
|
ui.label(format_angle(45f64.to_radians(), *au, *ap));
|
|
ui.end_row();
|
|
});
|
|
ui.separator();
|
|
ui.horizontal(|ui| {
|
|
apply = ui.button("OK").clicked();
|
|
cancel = ui.button("Cancel").clicked();
|
|
});
|
|
});
|
|
if apply {
|
|
let [lu, lp, au, ap, ins] = v;
|
|
let _ = app.run("units", json!({ "lunits": lu, "luprec": lp, "aunits": au, "auprec": ap, "insunits": ins }));
|
|
}
|
|
if apply || cancel || !*open {
|
|
ctx.data_mut(|d| d.remove::<[i64; 5]>(id));
|
|
*open = false;
|
|
} else {
|
|
ctx.data_mut(|d| d.insert_temp(id, v));
|
|
}
|
|
}
|
|
|
|
fn about(ctx: &egui::Context, open: &mut bool) {
|
|
let t = Tokens::get();
|
|
let tab_id = egui::Id::new("about_tab");
|
|
egui::Window::new(crate::tl!("About CADCraft"))
|
|
.id(egui::Id::new("About CADCraft"))
|
|
.open(open)
|
|
.default_size(vec2(640.0, 420.0))
|
|
.collapsible(false)
|
|
.show(ctx, |ui| {
|
|
let mut tab = ui.data_mut(|d| d.get_temp::<u8>(tab_id)).unwrap_or(0);
|
|
ui.horizontal(|ui| {
|
|
for (i, l) in ["About", "Contributors", "Models"].iter().enumerate() {
|
|
if ui.selectable_label(tab == i as u8, crate::i18n::t(l)).clicked() {
|
|
tab = i as u8;
|
|
}
|
|
}
|
|
});
|
|
ui.data_mut(|d| d.insert_temp(tab_id, tab));
|
|
ui.separator();
|
|
match tab {
|
|
1 => crate::credits::contributors_ui(ui),
|
|
2 => crate::credits::models_ui(ui),
|
|
_ => {
|
|
ui.heading(crate::tl!("CADCraft"));
|
|
ui.label(crate::tf!("Version {version}", version = env!("CARGO_PKG_VERSION")));
|
|
ui.label(crate::tl!("Computer-aided design and drafting: an open-source, clean-room CAD application written in pure Rust."));
|
|
ui.add_space(6.0);
|
|
ui.label(RichText::new(crate::tl!("One of the Crafting Apps by the ArtCraft team and community.")).color(t.text_dim));
|
|
ui.hyperlink_to("getartcraft.com/apps/cadcraft", "https://getartcraft.com/apps/cadcraft");
|
|
ui.hyperlink_to(crate::tl!("Join us on Discord"), "https://discord.gg/artcraft");
|
|
ui.add_space(6.0);
|
|
ui.label(RichText::new(crate::tl!("MIT OR Apache-2.0. Not affiliated with Autodesk, Inc.")).small().color(t.text_faint));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn commands(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
|
let mut start = None;
|
|
egui::Window::new(crate::tl!("Command Reference")).id(egui::Id::new("Command Reference")).open(open).default_size(vec2(640.0, 480.0)).show(
|
|
ctx,
|
|
|ui| {
|
|
let id = ui.id().with("cmdfilter");
|
|
let mut filter = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_default();
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Filter"));
|
|
ui.text_edit_singleline(&mut filter);
|
|
});
|
|
ui.data_mut(|d| d.insert_temp(id, filter.clone()));
|
|
let f = filter.to_lowercase();
|
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
egui::Grid::new("cmdref").striped(true).num_columns(3).show(ui, |ui| {
|
|
for c in cadcraft_engine::command_specs() {
|
|
if !f.is_empty()
|
|
&& !c.id.contains(&f)
|
|
&& !c.label.to_ascii_lowercase().contains(&f)
|
|
&& !crate::i18n::t(c.label).to_lowercase().contains(&f)
|
|
{
|
|
continue;
|
|
}
|
|
if ui.link(c.id.to_ascii_uppercase()).clicked() {
|
|
start = Some(c.id);
|
|
}
|
|
ui.label(crate::i18n::t(c.label));
|
|
ui.label(RichText::new(if c.aliases.is_empty() { String::new() } else { c.aliases.join(", ").to_ascii_uppercase() }).small());
|
|
ui.end_row();
|
|
}
|
|
});
|
|
});
|
|
},
|
|
);
|
|
if let Some(c) = start {
|
|
app.ui.dialog = None;
|
|
app.start(c);
|
|
}
|
|
}
|
|
|
|
fn blocks(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
|
let mut pick = None;
|
|
egui::Window::new(crate::tl!("Blocks")).id(egui::Id::new("Blocks")).open(open).default_size(vec2(320.0, 360.0)).show(ctx, |ui| {
|
|
let Ok(d) = app.session.doc() else { return };
|
|
let names: Vec<&String> = d.blocks.keys().filter(|k| !k.starts_with('*')).collect();
|
|
if names.is_empty() {
|
|
ui.label(crate::tl!("No blocks defined in this drawing."));
|
|
}
|
|
for n in names {
|
|
if ui.button(n).on_hover_text(crate::tl!("Insert this block")).clicked() {
|
|
pick = Some(n.clone());
|
|
}
|
|
}
|
|
});
|
|
if let Some(n) = pick {
|
|
insert_block(app, &n);
|
|
}
|
|
}
|
|
|
|
/// Start INSERT with `name` already given at its block name prompt, so the next prompt asks for
|
|
/// the insertion point.
|
|
fn insert_block(app: &mut CadApp, name: &str) {
|
|
app.start("insert");
|
|
if app.session.running.as_ref().is_none_or(|r| r.id != "insert") {
|
|
return;
|
|
}
|
|
if let Err(e) = app.session.input(cadcraft_engine::Input::Text(name.to_string())) {
|
|
app.session.echo(e.to_string());
|
|
}
|
|
}
|
|
|
|
/// The editor's lines as a list: a hanging indent of three text heights (`height` is one, in
|
|
/// drawing units, as `\p` indents are) with a tab stop there, and each line a bullet or its
|
|
/// number and a tab.
|
|
fn as_list(buf: &str, numbered: bool, height: f64) -> String {
|
|
let lines: Vec<String> =
|
|
buf.split('\n').enumerate().map(|(i, l)| if numbered { format!("{}.\t{l}", i + 1) } else { format!("•\t{l}") }).collect();
|
|
let indent = format!("{:.6}", 3.0 * height);
|
|
let indent = indent.trim_end_matches('0').trim_end_matches('.');
|
|
format!("\\pxi-{indent},l{indent},t{indent};{}", lines.join("\n"))
|
|
}
|
|
|
|
/// The multiline text editor shown while MTEXT asks for its contents.
|
|
fn mtext_editor(app: &mut CadApp, ctx: &egui::Context) {
|
|
let active = app.session.running.as_ref().is_some_and(|r| r.id == "mtext")
|
|
&& app.session.current_prompt().is_some_and(|p| p.accept.text && !p.accept.point);
|
|
let id = egui::Id::new("mtext_editor_buffer");
|
|
if !active {
|
|
ctx.data_mut(|d| d.remove::<String>(id));
|
|
return;
|
|
}
|
|
let mut buf = ctx.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_default();
|
|
let mut submit = None;
|
|
let mut cancel = false;
|
|
egui::Window::new(crate::tl!("Text Editor"))
|
|
.id(egui::Id::new("Text Editor"))
|
|
.collapsible(false)
|
|
.resizable(true)
|
|
.default_size(vec2(460.0, 220.0))
|
|
.show(ctx, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label(crate::tl!("Style: Standard"));
|
|
ui.separator();
|
|
let h = app.session.doc().map(|d| d.header.f64("TEXTSIZE", 0.2)).unwrap_or(0.2);
|
|
ui.label(crate::tf!("Height: {height}", height = format!("{h:.4}")));
|
|
ui.separator();
|
|
if ui.button(crate::tl!("B")).on_hover_text(crate::tl!("Bold")).clicked() {
|
|
buf.push_str("{\\fArial|b1;}");
|
|
}
|
|
if ui.button("⅟").on_hover_text(crate::tl!("Stack (type 1/2 then select)")).clicked() {
|
|
buf.push_str("\\S1/2;");
|
|
}
|
|
if ui.button("°").on_hover_text(crate::tl!("Degree")).clicked() {
|
|
buf.push_str("%%d");
|
|
}
|
|
if ui.button("±").on_hover_text(crate::tl!("Plus/minus")).clicked() {
|
|
buf.push_str("%%p");
|
|
}
|
|
if ui.button("⌀").on_hover_text(crate::tl!("Diameter")).clicked() {
|
|
buf.push_str("%%c");
|
|
}
|
|
ui.separator();
|
|
if ui.button("⇥").on_hover_text(crate::tl!("Insert tab")).clicked() {
|
|
buf.push('\t');
|
|
}
|
|
if ui.button("•≡").on_hover_text(crate::tl!("Bulleted list")).clicked() {
|
|
buf = as_list(&buf, false, h);
|
|
}
|
|
if ui.button("1≡").on_hover_text(crate::tl!("Numbered list")).clicked() {
|
|
buf = as_list(&buf, true, h);
|
|
}
|
|
});
|
|
let r = ui.add(
|
|
egui::TextEdit::multiline(&mut buf)
|
|
.desired_rows(6)
|
|
.desired_width(f32::INFINITY)
|
|
.hint_text(crate::tl!("Type text; Enter starts a new paragraph")),
|
|
);
|
|
if !r.has_focus() && buf.is_empty() {
|
|
r.request_focus();
|
|
}
|
|
ui.horizontal(|ui| {
|
|
if ui.button(crate::tl!("OK")).clicked() || (r.has_focus() && ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Enter))) {
|
|
submit = Some(buf.replace('\n', "\\P"));
|
|
}
|
|
if ui.button(crate::tl!("Cancel")).clicked() {
|
|
cancel = true;
|
|
}
|
|
let finish = if ui.ctx().os().is_mac() { crate::tl!("⌘↩ to finish") } else { crate::tl!("Ctrl+Enter to finish") };
|
|
ui.label(RichText::new(finish).small());
|
|
});
|
|
});
|
|
ctx.data_mut(|d| d.insert_temp(id, buf));
|
|
if let Some(t) = submit {
|
|
ctx.data_mut(|d| d.remove::<String>(id));
|
|
// The editor holds the whole contents: one input, then Enter ends MTEXT's text entry.
|
|
if !t.is_empty() {
|
|
let _ = app.session.input(cadcraft_engine::Input::Text(t));
|
|
}
|
|
if app.session.running.as_ref().is_some_and(|r| r.id == "mtext") {
|
|
let _ = app.session.input(cadcraft_engine::Input::Enter);
|
|
}
|
|
} else if cancel {
|
|
ctx.data_mut(|d| d.remove::<String>(id));
|
|
app.session.cancel();
|
|
}
|
|
}
|
|
|
|
fn language(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {
|
|
egui::Window::new(crate::tl!("Interface Language")).id(egui::Id::new("interface_language")).open(open).resizable(false).show(ctx, |ui| {
|
|
egui::ComboBox::from_id_salt("interface_language_choice").selected_text(app.ui.interface_language.name()).show_ui(ui, |ui| {
|
|
for preference in crate::i18n::Preference::ALL {
|
|
if ui.selectable_label(app.ui.interface_language == preference, preference.name()).clicked() {
|
|
let _ = app.run("ui.language", serde_json::json!({"lang": preference.code()}));
|
|
ui.ctx().request_repaint();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use cadcraft_engine::doc::EntityKind;
|
|
use cadcraft_engine::geom::Vec2;
|
|
use cadcraft_engine::{Input, Session};
|
|
use serde_json::json;
|
|
|
|
use crate::{CadApp, Services};
|
|
|
|
#[test]
|
|
fn picking_a_block_starts_insert_with_its_name() {
|
|
let mut app = CadApp::new(Session::new(), Services::default());
|
|
let c = app.session.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
|
|
let h = c["handle"].as_str().unwrap().to_string();
|
|
app.session.execute("block", &json!({"name": "Door 2", "base": [0, 0], "handles": [h], "keep": "delete"})).unwrap();
|
|
// A command already running is replaced, as when a command is typed.
|
|
app.start("line");
|
|
|
|
super::insert_block(&mut app, "Door 2");
|
|
assert!(app.session.running.as_ref().is_some_and(|r| r.id == "insert"));
|
|
assert_eq!(app.session.current_prompt().map(|p| p.message), Some("Specify insertion point".to_string()));
|
|
|
|
app.session.input(Input::Point(Vec2::new(5.0, 6.0))).unwrap();
|
|
// X scale, Y scale and rotation take their defaults.
|
|
for _ in 0..3 {
|
|
app.session.input(Input::Enter).unwrap();
|
|
}
|
|
assert!(app.session.running.is_none());
|
|
let d = app.session.doc().unwrap();
|
|
let mut ins = Vec::new();
|
|
for e in d.model.iter() {
|
|
if let EntityKind::Insert(i) = &e.kind {
|
|
ins.push(i.clone());
|
|
}
|
|
}
|
|
assert_eq!(ins.len(), 1);
|
|
assert_eq!(ins[0].block, "Door 2");
|
|
assert!((ins[0].insert.x - 5.0).abs() < 1e-9 && (ins[0].insert.y - 6.0).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn picking_a_missing_block_keeps_asking_for_a_name() {
|
|
let mut app = CadApp::new(Session::new(), Services::default());
|
|
super::insert_block(&mut app, "Nope");
|
|
assert!(app.session.running.as_ref().is_some_and(|r| r.id == "insert"));
|
|
assert_eq!(app.session.current_prompt().map(|p| p.message), Some("Enter block name".to_string()));
|
|
}
|
|
}
|