Cargo: codec 76d2a1c, kernel e652b4d, graph 0545fa9; text styles, templates, solid edges

- Text styles keep their TrueType typeface through save and reopen (codec
  reads and writes the STYLE record's ACAD extended data) (#1633)
- Unnamed shape-file entries no longer show in text style lists (#1634)
- Save As offers DWT 2018; NEW asks for the template to start from, while
  Ctrl+N and the quick-access New run QNEW with the default template (#1635)
- An edge next to an earlier fillet of the same radius rounds together with
  it (kernel); the solid stays drawn and pickable while edges are chosen
  (#1625, #1107)
- A full cache rebuild drops the B-rep cache too, so an undone fillet does
  not come back with the next edge operation (#1107)
- Selected solids in shaded styles highlight only their visible edges (#1031)
- VIEW ISO / SWISO / SEISO / NEISO / NWISO snap to the isometric corners

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-06 18:32:43 +03:00
1 parent 1cbcc8f4c5
commit 1eae36e726
43 files changed
+152 -34

No files matched your search

+19 -1
View File
@@ -3,7 +3,25 @@ use super::*;
impl OpenCADStudio {
pub(super) fn dispatch_fileops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"NEW" => return Some(Task::done(Message::TabNew)),
// QNEW starts from the default template (Options > Files); NEW
// asks which template to start from (#1635).
"QNEW" => return Some(Task::done(Message::TabNew)),
"NEW" => {
return Some(Task::perform(
async {
crate::sys::file_dialog()
.set_title(crate::t!("Select Template").as_ref())
.add_filter(
crate::t!("Drawing Template (*.dwt)").as_ref(),
&["dwt", "DWT", "dwg", "DWG", "dxf", "DXF"],
)
.pick_file()
.await
.map(|handle| crate::sys::handle_path(&handle))
},
Message::NewFromTemplate,
));
}
"OPEN" => return Some(Task::done(Message::OpenFile)),
"SAVE" | "QSAVE" => return Some(Task::done(Message::SaveFile)),
// SAVEALL — write every open drawing that already has a file path.
+19 -2
View File
@@ -875,8 +875,25 @@ impl OpenCADStudio {
crate::scene::CubeRegion::Face(face),
)));
}
"ISO" | "ISOMETRIC" | "SWISO" => {
return Some(Task::done(Message::ViewCubeHome));
// Isometric presets snap to the matching ViewCube corner;
// ISO alone is the south-west one.
"ISO" | "ISOMETRIC" | "SWISO" | "SEISO" | "NEISO" | "NWISO" => {
let (x, y) = match sub.as_str() {
"SEISO" => (1.0, -1.0),
"NEISO" => (1.0, 1.0),
"NWISO" => (-1.0, 1.0),
_ => (-1.0, -1.0),
};
let want = glam::Vec3::new(x, y, 1.0).normalize();
let corner = (18..26)
.map(crate::scene::CubeRegion::Corner)
.max_by(|a, b| {
a.snap_direction()
.dot(want)
.total_cmp(&b.snap_direction().dot(want))
})
.expect("the cube has corners");
return Some(Task::done(Message::ViewCubeSnap(corner)));
}
// VIEW <name> shortcut for restore
_ => {
+1
View File
@@ -850,6 +850,7 @@ inventory::submit!(crate::command::CommandRegistration {
"CHSPACE",
"NAVVCUBE",
"NEW",
"QNEW",
"OBJIMPORT",
"OPEN",
"ORTHO",
+6 -1
View File
@@ -149,7 +149,12 @@ impl OpenCADStudio {
}
let doc = &self.tabs[i].scene.document;
let text_names: Vec<String> = doc.text_styles.iter().map(|s| s.name.clone()).collect();
let text_names: Vec<String> = doc
.text_styles
.iter()
.filter(|s| !s.is_shape_file)
.map(|s| s.name.clone())
.collect();
let active_text = doc.header.current_text_style_name.clone();
let active_text = if text_names.contains(&active_text) {
active_text
+2
View File
@@ -2299,6 +2299,8 @@ pub enum Message {
/// Options → Files: the default template for new drawings.
QnewTemplateChanged(String),
QnewTemplateBrowse,
/// NEW: the picked template a new drawing starts from (`None` = cancelled).
NewFromTemplate(Option<std::path::PathBuf>),
/// Set the default type/version used when first saving a new drawing.
DefaultSaveFormatChanged(String),
/// Select one of Iced's built-in themes or the editable Custom theme.
+1 -1
View File
@@ -27,7 +27,7 @@ pub(super) fn default_bindings() -> BTreeMap<String, String> {
("F12".to_string(), "DYNINPUT"),
(format!("{ACCEL}+0"), "CLEANSCREEN"),
(format!("{ACCEL}+1"), "PROPERTIES"),
(format!("{ACCEL}+N"), "NEW"),
(format!("{ACCEL}+N"), "QNEW"),
(format!("{ACCEL}+O"), "OPEN"),
(format!("{ACCEL}+P"), "PLOT"),
(format!("{ACCEL}+Q"), "QUIT"),
+8 -1
View File
@@ -234,7 +234,14 @@ impl OpenCADStudio {
v
};
match kind {
StyleKind::Text => doc.text_styles.iter().map(|s| s.name.clone()).collect(),
// Shape-file entries (complex linetype glyphs) are not text styles to
// pick or edit; they have no name (#1634).
StyleKind::Text => doc
.text_styles
.iter()
.filter(|s| !s.is_shape_file)
.map(|s| s.name.clone())
.collect(),
StyleKind::Dim => doc.dim_styles.iter().map(|s| s.name.clone()).collect(),
StyleKind::Table => from_objects(|o| match o {
ObjectType::TableStyle(s) => Some(s.name.as_str()),
+2
View File
@@ -3137,6 +3137,8 @@ impl OpenCADStudio {
let default_name = filename;
let (filter_label, filter_ext): (&str, &str) = if ext.eq_ignore_ascii_case("dxf") {
("DXF Files", "dxf")
} else if ext.eq_ignore_ascii_case("dwt") {
("Drawing Template (*.dwt)", "dwt")
} else {
("DWG Files", "dwg")
};
+16
View File
@@ -8028,6 +8028,22 @@ impl OpenCADStudio {
self.persist_settings_if_changed();
Task::none()
}
Message::NewFromTemplate(Some(path)) => {
let task = self.update(Message::TabNew);
let idx = self.active_tab;
match self.apply_template(&path.to_string_lossy()) {
Ok(_) => {
self.tabs[idx].dirty = false;
self.refresh_layer_panel();
self.sync_ribbon_styles();
}
Err(error) => self
.command_line
.push_error(error["error"].as_str().unwrap_or("template")),
}
task
}
Message::NewFromTemplate(None) => Task::none(),
Message::QnewTemplateBrowse => Task::perform(
async {
crate::sys::file_dialog()
+8 -2
View File
@@ -1152,7 +1152,9 @@ impl OpenCADStudio {
let mut lt_opts: Vec<String> = vec!["ByBlock".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()));
textstyle_opts.extend(
doc.text_styles.iter().filter(|t| !t.is_shape_file).map(|t| t.name.clone()),
);
let opt_block = |h: Option<codec::types::Handle>| -> String {
match h {
Some(h) => doc
@@ -1416,7 +1418,11 @@ impl OpenCADStudio {
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
let text_style_opts: Vec<String> =
doc.text_styles.iter().map(|style| style.name.clone()).collect();
doc.text_styles
.iter()
.filter(|style| !style.is_shape_file)
.map(|style| style.name.clone())
.collect();
let text_style_fixed_height = doc
.text_styles
.get(&self.ds_dimtxsty)
+12 -5
View File
@@ -1402,7 +1402,7 @@ pub const DEFAULT_SAVE_FORMAT: &str = "DWG 2018";
pub const SAVE_FORMAT_OPTIONS: &[&str] = &[
"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",
"DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14", "DWT 2018",
];
pub fn canonical_save_format(format: &str) -> &'static str {
@@ -1432,7 +1432,14 @@ pub fn parse_save_format(format: &str) -> (&'static str, codec::DxfVersion) {
use codec::DxfVersion;
let f = format.to_ascii_uppercase();
let is_dxf = f.starts_with("DXF");
let ext = if is_dxf { "dxf" } else { "dwg" };
// A drawing template (.dwt) is a DWG under another extension (#1635).
let ext = if is_dxf {
"dxf"
} else if f.starts_with("DWT") {
"dwt"
} else {
"dwg"
};
let version = if f.contains("2013") {
DxfVersion::AC1027
} else if f.contains("2010") {
@@ -1611,14 +1618,14 @@ impl std::fmt::Display for SaveFailure {
impl std::error::Error for SaveFailure {}
const SUPPORTED_SAVE_FORMATS: &str = ".dwg, .dxf";
const SUPPORTED_SAVE_FORMATS: &str = ".dwg, .dxf, .dwt";
fn validate_save_extension(path: &Path) -> Result<(), SaveFailure> {
let extension = path
.extension()
.map(|value| value.to_string_lossy().to_lowercase())
.unwrap_or_default();
if matches!(extension.as_str(), "dwg" | "dxf" | "sv$") {
if matches!(extension.as_str(), "dwg" | "dxf" | "dwt" | "sv$") {
return Ok(());
}
@@ -1684,7 +1691,7 @@ mod save_failure_tests {
assert_eq!(
error,
"unsupported output format .pdf; supported formats: .dwg, .dxf"
"unsupported output format .pdf; supported formats: .dwg, .dxf, .dwt"
);
assert!(!path.exists(), "unsupported export created an output file");
}
+1
View File
@@ -4878,6 +4878,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Unknown command: {}" => Some(("command", "unknown-command-cmd")),
"Unsaved changes will be discarded." => Some(("common", "unsaved-changes-will-be-discarded")),
"Discard & close" => Some(("common", "discard-close")),
"Select Template" => Some(("common", "select-template-title")),
"CHSPACE: Command not allowed in the Model tab." => Some(("common", "chspace-model-tab")),
"CHSPACE: No plan-view viewport to change space through." => Some(("common", "chspace-no-viewport")),
"CHSPACE: {count} object(s) changed to paper space." => Some(("common", "chspace-to-paper")),
+9 -1
View File
@@ -230,7 +230,15 @@ impl SolidEdgeCommand {
self.preview_for_values(self.default_value, self.other_value)
{
self.preview_wires = wires;
self.preview_hidden.push(handle);
// While edges are still being picked the source stays drawn: a
// hidden solid leaves the pick meshes, so the next edge could not
// be selected (#1625). The preview overlays it until confirmed.
if !matches!(
self.step,
EdgeStep::Selecting | EdgeStep::PickingLoop | EdgeStep::LoopConfirm
) {
self.preview_hidden.push(handle);
}
}
}
+4
View File
@@ -2575,6 +2575,10 @@ impl Scene {
if !incremental {
self.meshes.clear();
self.block_meshes.clear();
// The B-rep cache follows the entities too: an undo that rebuilds
// everything left the undone fillet's body here, and the next
// edge operation brought it back (#1107). Restored on demand.
self.solid_models.clear();
}
// BLOCK-entity handles of the layout (model + paper) blocks. A solid
// owned by one of these is top-level; anything else lives in a block
+8 -5
View File
@@ -4960,7 +4960,10 @@ analytic={:.1} regular={:.1} blocks={:.1}",
// ── Pass 7: selection overlay pass ───────────────────────────────
// Redraws selected wires and text with depth_compare=Always so both
// appear on top of all other geometry at full brightness.
// appear on top of all other geometry at full brightness — unless
// faces are drawn: then edges behind a surface stay hidden while
// selected, as they are when not (#1031).
let occluded = hidden_line || !mesh_wireframe;
let have_text_highlight =
(!self.text_highlight_gpu.is_empty())
|| !self.block_text_highlight_gpu.is_empty();
@@ -4997,7 +5000,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref);
if !self.gpu_selected_wires.is_empty() {
pass.set_pipeline(if hidden_line {
pass.set_pipeline(if occluded {
&self.wire_pipeline
} else {
&self.wire_xray_pipeline
@@ -5016,7 +5019,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
}
}
if !self.gpu_selected_block_wires.is_empty() {
pass.set_pipeline(if hidden_line {
pass.set_pipeline(if occluded {
&self.block_wire_pipeline
} else {
&self.block_wire_xray_pipeline
@@ -5029,7 +5032,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
}
}
if !self.gpu_selected_circles.is_empty() {
pass.set_pipeline(if hidden_line {
pass.set_pipeline(if occluded {
&self.circle_pipeline
} else {
&self.circle_xray_pipeline
@@ -5043,7 +5046,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
}
}
if !self.gpu_selected_ellipses.is_empty() {
pass.set_pipeline(if hidden_line {
pass.set_pipeline(if occluded {
&self.ellipse_pipeline
} else {
&self.ellipse_xray_pipeline
+1 -1
View File
@@ -521,7 +521,7 @@ impl Ribbon {
) -> Element<'_, Message> {
// ── Quick-access file commands + undo/redo, one merged flow ────────
let lead = iced::widget::Row::with_children(vec![
quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW").into(),
quick_access_btn(crate::ui::icons::DOC_NEW, "New", "QNEW").into(),
quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN").into(),
quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE").into(),
quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS").into(),
+2 -1
View File
@@ -425,7 +425,8 @@ pub fn view_window<'a>(
text(crate::tr!("options", "default-save-format-label")).size(12).width(150),
iced::widget::pick_list(
selected_format,
crate::io::SAVE_FORMAT_OPTIONS,
// Drawings, not the template entry (the list's last).
&crate::io::SAVE_FORMAT_OPTIONS[..crate::io::SAVE_FORMAT_OPTIONS.len() - 1],
|value| value.to_string(),
)
.on_select(|format: &str| Message::DefaultSaveFormatChanged(format.to_string()))