style: apply cargo fmt across workspace

This commit is contained in:
Karim Jerbi committed 2026-10-11 03:14:29 +01:00
1 parent 85710e5454
commit 9cf9ff2c5c
531 files changed
+57740 -33906

No files matched your search

+296 -97
View File
@@ -49,23 +49,46 @@ fn color_value(color: codec::types::Color) -> Value {
/// `kind`, `value`, `options`). The node graph moves values in this form too,
/// so a port's value round-trips through [`OpenCADStudio::set_property_value`].
pub(crate) fn property_json(p: &crate::scene::model::object::Property) -> Value {
let (kind,value,options)=match &p.value{
PropValue::ReadOnly(v)|PropValue::ReadOnlyWithTooltip{value:v,..}=>("readonly",json!(v),Value::Null),
PropValue::EditText(v)=>("number",json!(v),Value::Null),
PropValue::PlainText(v)=>("text",json!(v),Value::Null),
PropValue::Hyperlink(v)=>("hyperlink",json!(v),Value::Null),
PropValue::Choice{selected,options}=>("choice",json!(selected),json!(options)),
PropValue::EditChoice{value,options}=>("editable_choice",json!(value),json!(options)),
PropValue::LayerChoice(v)=>("layer",json!(v),Value::Null),
PropValue::LinetypeChoice(v)=>("linetype",json!(v),Value::Null),
PropValue::BoolToggle{value,..}=>("bool",json!(value),Value::Null),
PropValue::ColorChoice(value)|PropValue::NamedColorChoice{color:value,..}=>("color",color_value(*value),Value::Null),
PropValue::ColorVaries=>("color",Value::Null,Value::Null),
PropValue::LwChoice(value)|PropValue::FieldLwChoice{value,..}=>("lineweight",json!(value.value()),json!(crate::ui::properties::lw_options().iter().map(|v|v.0.value()).collect::<Vec<_>>())),
PropValue::LwVaries|PropValue::FieldLwVaries{..}=>("lineweight",Value::Null,json!(crate::ui::properties::lw_options().iter().map(|v|v.0.value()).collect::<Vec<_>>())),
PropValue::AttrText{tag,value}=>("attribute",json!({"tag":tag,"value":value}),Value::Null),
other=>("specialized",json!(format!("{other:?}")),Value::Null),
};json!({"id":p.field,"label":p.label,"kind":kind,"value":value,"options":options})
let (kind, value, options) = match &p.value {
PropValue::ReadOnly(v) | PropValue::ReadOnlyWithTooltip { value: v, .. } => {
("readonly", json!(v), Value::Null)
}
PropValue::EditText(v) => ("number", json!(v), Value::Null),
PropValue::PlainText(v) => ("text", json!(v), Value::Null),
PropValue::Hyperlink(v) => ("hyperlink", json!(v), Value::Null),
PropValue::Choice { selected, options } => ("choice", json!(selected), json!(options)),
PropValue::EditChoice { value, options } => {
("editable_choice", json!(value), json!(options))
}
PropValue::LayerChoice(v) => ("layer", json!(v), Value::Null),
PropValue::LinetypeChoice(v) => ("linetype", json!(v), Value::Null),
PropValue::BoolToggle { value, .. } => ("bool", json!(value), Value::Null),
PropValue::ColorChoice(value) | PropValue::NamedColorChoice { color: value, .. } => {
("color", color_value(*value), Value::Null)
}
PropValue::ColorVaries => ("color", Value::Null, Value::Null),
PropValue::LwChoice(value) | PropValue::FieldLwChoice { value, .. } => (
"lineweight",
json!(value.value()),
json!(crate::ui::properties::lw_options()
.iter()
.map(|v| v.0.value())
.collect::<Vec<_>>()),
),
PropValue::LwVaries | PropValue::FieldLwVaries { .. } => (
"lineweight",
Value::Null,
json!(crate::ui::properties::lw_options()
.iter()
.map(|v| v.0.value())
.collect::<Vec<_>>()),
),
PropValue::AttrText { tag, value } => {
("attribute", json!({"tag":tag,"value":value}), Value::Null)
}
other => ("specialized", json!(format!("{other:?}")), Value::Null),
};
json!({"id":p.field,"label":p.label,"kind":kind,"value":value,"options":options})
}
pub(super) const NAMES: &[&str] = &[
"close_modal",
@@ -242,7 +265,10 @@ impl OpenCADStudio {
parse_calibrate(&dummy, image.pixel_width, image.pixel_height)?
} else {
let default_width = (image.pixel_width as f64 / 100.0).max(1.0);
let width = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(default_width);
let width = req["width"]
.as_f64()
.filter(|w| *w > 0.0)
.unwrap_or(default_width);
let scale = width / image.pixel_width as f64;
(embed_point(req)?, width, scale)
};
@@ -413,13 +439,13 @@ impl OpenCADStudio {
Some(base) => {
let mut base = base.clone();
crate::modules::insert::wblock::extract_entities_into(
document,
&handles?,
&mut base,
document, &handles?, &mut base,
)?;
Ok(base)
}
None => crate::modules::insert::wblock::extract_entities_to_doc(document, &handles?),
None => {
crate::modules::insert::wblock::extract_entities_to_doc(document, &handles?)
}
}
} else {
return Err(failure(
@@ -489,7 +515,10 @@ impl OpenCADStudio {
.find(|name| name.eq_ignore_ascii_case(&requested))
.cloned()
.ok_or_else(|| {
failure("layout_missing", format!("Layout '{requested}' does not exist"))
failure(
"layout_missing",
format!("Layout '{requested}' does not exist"),
)
})?]
};
@@ -570,9 +599,15 @@ impl OpenCADStudio {
"limits" => request.area = "Limits".into(),
"layout" => request.area = "Layout".into(),
"window" => {
let values = req["window"].as_array().filter(|v| v.len() == 4).ok_or_else(|| {
failure("window_required", "area \"window\" needs window:[x0,y0,x1,y1]")
})?;
let values = req["window"]
.as_array()
.filter(|v| v.len() == 4)
.ok_or_else(|| {
failure(
"window_required",
"area \"window\" needs window:[x0,y0,x1,y1]",
)
})?;
let coord = |k: usize| {
values[k].as_f64().filter(|v| v.is_finite()).ok_or_else(|| {
failure("invalid_window", "window must be four finite numbers")
@@ -587,7 +622,11 @@ impl OpenCADStudio {
// Snapshot everything the pipeline reads or mutates, like print-all.
let original_layout = self.tabs[i].scene.current_layout.clone();
let original_viewport = self.tabs[i].scene.active_viewport;
let original_psltscale = self.tabs[i].scene.document.header.paper_space_linetype_scaling;
let original_psltscale = self.tabs[i]
.scene
.document
.header
.paper_space_linetype_scaling;
let original_plimcheck = self.tabs[i].scene.document.header.paper_space_limit_check;
let original_dialog = self.plot_dialog.clone();
let original_window = self.plot_window;
@@ -670,8 +709,7 @@ impl OpenCADStudio {
}
_ => None,
};
let page =
page.ok_or_else(|| format!("Layout '{name}' plot area is empty."))?;
let page = page.ok_or_else(|| format!("Layout '{name}' plot area is empty."))?;
pages.push(page);
}
Ok(pages)
@@ -679,7 +717,11 @@ impl OpenCADStudio {
let built = build(self);
self.tabs[i].scene.current_layout = original_layout;
self.tabs[i].scene.active_viewport = original_viewport;
self.tabs[i].scene.document.header.paper_space_linetype_scaling = original_psltscale;
self.tabs[i]
.scene
.document
.header
.paper_space_linetype_scaling = original_psltscale;
self.tabs[i].scene.document.header.paper_space_limit_check = original_plimcheck;
self.plot_dialog = original_dialog;
self.plot_window = original_window;
@@ -695,7 +737,13 @@ impl OpenCADStudio {
for (name, page) in targets.iter().zip(&pages) {
let safe: String = name
.chars()
.map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
let file = format!("{stem}-{safe}.pdf");
crate::io::pdf_export::export_pdf_pages(
@@ -762,7 +810,8 @@ impl OpenCADStudio {
let target = handle(req)?;
let grip_id = req["grip_id"]
.as_u64()
.ok_or_else(|| failure("invalid_request", "Missing grip_id"))? as usize;
.ok_or_else(|| failure("invalid_request", "Missing grip_id"))?
as usize;
let to = point(req)?;
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(target, false);
@@ -976,9 +1025,11 @@ impl OpenCADStudio {
"close_modal" => Message::CloseModal,
// The open PDF dialog's OK button.
// Underlay Layers: switch a layer of the shown underlay.
"pdf_layer_toggle" => Message::PdfDialog(
crate::ui::window::pdf_dialogs::PdfDialogMsg::LayersToggle(string(req, "value")?.into()),
),
"pdf_layer_toggle" => {
Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::LayersToggle(
string(req, "value")?.into(),
))
}
// Point Cloud Color Map: one edit, "tab=elevation", "count=3",
// "even", "reverse", "scheme=Earth", "gradient=0", "max=90",
// "min=10", "interval=0.1", "extents=0", "range=2", "current=0".
@@ -990,7 +1041,9 @@ impl OpenCADStudio {
Message::PdfDialog(match key {
"tab" => M::MapTab(arg == "elevation"),
"scheme" => M::MapScheme(arg.into()),
"count" => M::MapCount(arg.parse().map_err(|_| failure("invalid_value", "count"))?),
"count" => {
M::MapCount(arg.parse().map_err(|_| failure("invalid_value", "count"))?)
}
"even" => M::MapEven,
"reverse" => M::MapReverse,
"gradient" => M::MapGradient(on),
@@ -1033,7 +1086,10 @@ impl OpenCADStudio {
use crate::ui::window::pc_manager::{PcManagerMsg, Row};
let value = string(req, "value")?;
let parsed = value.split_once(':').and_then(|(handle, row)| {
Some((codec::Handle::new(u64::from_str_radix(handle, 16).ok()?), Row::parse(row)?))
Some((
codec::Handle::new(u64::from_str_radix(handle, 16).ok()?),
Row::parse(row)?,
))
});
let Some((handle, row)) = parsed else {
return Err(failure("bad_value", "Expected <handle>:<row>"));
@@ -1062,8 +1118,14 @@ impl OpenCADStudio {
let [l, s, m] = self.count_palette.expansion(name).ok_or_else(bad)?;
let i = self.active_tab;
let area = self.tabs[i].count.as_ref().and_then(|c| c.area.clone());
let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref());
let (k, _) = codec::count::expanded_counts(&instances, name, l, s, m).into_iter().nth(n).ok_or_else(bad)?;
let instances = codec::count::block_instances(
&self.tabs[i].scene.document,
area.as_deref(),
);
let (k, _) = codec::count::expanded_counts(&instances, name, l, s, m)
.into_iter()
.nth(n)
.ok_or_else(bad)?;
M::Open(name.into(), k)
}
"menu" => {
@@ -1089,7 +1151,10 @@ impl OpenCADStudio {
"back" => M::Back,
"match" => {
let (k, on) = arg.split_once(':').unwrap_or((arg, "1"));
M::Match(k.parse::<usize>().ok().filter(|k| *k < 3).ok_or_else(bad)?, flag(on))
M::Match(
k.parse::<usize>().ok().filter(|k| *k < 3).ok_or_else(bad)?,
flag(on),
)
}
"details" => M::ToggleDetails,
"errors" => M::ToggleErrors,
@@ -1097,7 +1162,13 @@ impl OpenCADStudio {
let i = self.active_tab;
let n: usize = arg.parse().map_err(|_| bad())?;
let mode = self.tabs[i].count.as_ref().ok_or_else(bad)?;
M::ShowError(*mode.result(&self.tabs[i].scene.document).errors.get(n).ok_or_else(bad)?)
M::ShowError(
*mode
.result(&self.tabs[i].scene.document)
.errors
.get(n)
.ok_or_else(bad)?,
)
}
"area" => M::Area,
"prev" => M::Prev,
@@ -1132,20 +1203,45 @@ impl OpenCADStudio {
// "drop=<item>@x,y", "menu=<action>:<item>". An item is
// "current:NAME", "recent:N", "fav:N", "lib:N" or "file:<path>".
"blocks_palette" => {
use crate::ui::window::block_palette::{BlockPaletteMsg as M, Item, MenuAction, OptionMsg, Tab, ViewMode};
use crate::ui::window::block_palette::{
BlockPaletteMsg as M, Item, MenuAction, OptionMsg, Tab, ViewMode,
};
let value = string(req, "value")?;
let (key, arg) = value.split_once('=').unwrap_or((value, ""));
let item = |app: &Self, spec: &str| -> Result<Item, Value> {
let (kind, rest) = spec.split_once(':').unwrap_or((spec, ""));
let index = || rest.parse::<usize>().map_err(|_| failure("invalid_value", "Bad item index"));
let index = || {
rest.parse::<usize>()
.map_err(|_| failure("invalid_value", "Bad item index"))
};
let missing = || failure("invalid_value", "No such palette item");
Ok(match kind {
"current" => Item::Current(rest.to_string()),
"recent" => Item::Ref(app.block_palette.recent.get(index()?).cloned().ok_or_else(missing)?),
"fav" => Item::Ref(app.block_palette.favorites.get(index()?).cloned().ok_or_else(missing)?),
"recent" => Item::Ref(
app.block_palette
.recent
.get(index()?)
.cloned()
.ok_or_else(missing)?,
),
"fav" => Item::Ref(
app.block_palette
.favorites
.get(index()?)
.cloned()
.ok_or_else(missing)?,
),
"lib" => {
let e = app.block_palette.library_entries.get(index()?).ok_or_else(missing)?;
if e.folder { Item::Folder(e.path.clone()) } else { Item::File(e.path.clone()) }
let e = app
.block_palette
.library_entries
.get(index()?)
.ok_or_else(missing)?;
if e.folder {
Item::Folder(e.path.clone())
} else {
Item::File(e.path.clone())
}
}
"file" => Item::File(std::path::PathBuf::from(rest)),
_ => return Err(missing()),
@@ -1159,7 +1255,9 @@ impl OpenCADStudio {
_ => Tab::Current,
})),
"search" => Message::BlockPalette(M::Search(arg.into())),
"view" => Message::BlockPalette(M::View(ViewMode::from_u8(arg.parse().unwrap_or(1)))),
"view" => {
Message::BlockPalette(M::View(ViewMode::from_u8(arg.parse().unwrap_or(1))))
}
"options" => Message::BlockPalette(M::ToggleOptions),
"library" => Message::BlockPalette(M::LibraryPicked(Ok(arg.into()))),
"up" => Message::BlockPalette(M::LibraryUp),
@@ -1187,19 +1285,27 @@ impl OpenCADStudio {
Message::BlockPalette(M::Release(it))
}
"drop" => {
let (spec, at) = arg.rsplit_once('@').ok_or_else(|| failure("invalid_value", "drop needs @x,y"))?;
let xy: Vec<f64> = at.split(',').filter_map(|v| v.trim().parse().ok()).collect();
let (spec, at) = arg
.rsplit_once('@')
.ok_or_else(|| failure("invalid_value", "drop needs @x,y"))?;
let xy: Vec<f64> = at
.split(',')
.filter_map(|v| v.trim().parse().ok())
.collect();
if xy.len() < 2 {
return Err(failure("invalid_value", "drop needs @x,y"));
}
let it = item(self, spec)?;
let i = self.active_tab;
self.tabs[i].last_cursor_world = glam::DVec3::new(xy[0], xy[1], xy.get(2).copied().unwrap_or(0.0));
self.tabs[i].last_cursor_world =
glam::DVec3::new(xy[0], xy[1], xy.get(2).copied().unwrap_or(0.0));
self.block_palette.pressed = Some(it);
Message::ViewportLeftRelease
}
"menu" => {
let (action, spec) = arg.split_once(':').ok_or_else(|| failure("invalid_value", "menu needs action:item"))?;
let (action, spec) = arg
.split_once(':')
.ok_or_else(|| failure("invalid_value", "menu needs action:item"))?;
let action = match action {
"insert" => MenuAction::Insert,
"redefine" => MenuAction::Redefine,
@@ -1296,26 +1402,46 @@ impl OpenCADStudio {
.filter(|i| *i < len)
.ok_or_else(|| failure("invalid_value", "index out of range"))
};
let named_len = self.field_dialog.as_ref().map_or(0, |d| d.named_names.len());
let named_len = self
.field_dialog
.as_ref()
.map_or(0, |d| d.named_names.len());
let known = |list: &[&'static str]| {
list.iter().copied().find(|n| n.eq_ignore_ascii_case(v)).ok_or_else(|| failure("invalid_value", "unknown name"))
list.iter()
.copied()
.find(|n| n.eq_ignore_ascii_case(v))
.ok_or_else(|| failure("invalid_value", "unknown name"))
};
Message::FieldDialog(match key {
"category" => F::Category(index(fd::CATEGORIES.len())?),
"name" => F::Name(known(&fd::fields_of(0))?),
"date" => F::DateFormat(v.into()),
"case" => F::TextCase(index(fd::TEXT_CASES.len())?),
"file" => F::FileParts(v.parse::<u8>().map_err(|_| failure("invalid_value", "1|2|3"))?),
"file" => F::FileParts(
v.parse::<u8>()
.map_err(|_| failure("invalid_value", "1|2|3"))?,
),
"ext" => F::FileExtension(v == "1"),
"size" => F::SizeUnit(index(fd::SIZE_UNITS.len())?),
"sysvar" => F::SysVar(v.into()),
"diesel" => F::Diesel(v.into()),
"named_type" => F::NamedType(index(fd::NAMED_TYPES.len())?),
"named" => F::Named(index(named_len)?),
"prop" => F::ObjectProp(known(&["Area", "Center", "Circumference", "Diameter", "EndPoint", "Length", "Radius", "StartPoint"])?),
"prop" => F::ObjectProp(known(&[
"Area",
"Center",
"Circumference",
"Diameter",
"EndPoint",
"Length",
"Radius",
"StartPoint",
])?),
"select_object" => F::SelectObject,
"formula" => F::Formula(v.into()),
"formula_format" => F::FormulaFormat(index(codec::fields::FORMULA_FORMATS.len())?),
"formula_format" => {
F::FormulaFormat(index(codec::fields::FORMULA_FORMATS.len())?)
}
"precision" => F::FormulaPrecision(index(fd::FORMULA_PRECISIONS.len())?),
"link_text" => F::HyperlinkText(v.into()),
"link_url" => F::HyperlinkUrl(v.into()),
@@ -1326,8 +1452,14 @@ impl OpenCADStudio {
"ss_set" => F::SsSet(v.parse().map_err(|_| failure("invalid_value", "index"))?),
"ss_dst" => F::SsPicked(Some(std::path::PathBuf::from(v))),
"ss_node" => {
let state = self.field_dialog.as_ref().ok_or_else(|| failure("invalid_state", "No Field dialog"))?;
let db = state.ss_sets.get(state.ss_set).ok_or_else(|| failure("invalid_state", "No sheet set"))?;
let state = self
.field_dialog
.as_ref()
.ok_or_else(|| failure("invalid_state", "No Field dialog"))?;
let db = state
.ss_sets
.get(state.ss_set)
.ok_or_else(|| failure("invalid_state", "No sheet set"))?;
let views = state.name == "SheetView";
let id = fd::nav_nodes(db, views)
.into_iter()
@@ -1340,7 +1472,9 @@ impl OpenCADStudio {
"ss_href" => F::SsHref(v == "1"),
"ss_placeholder" => F::SsPlaceholder(index(fd::PLACEHOLDER_TYPES.len())?),
"ss_scale" => F::SsScaleFormat(index(codec::fields::PLOT_SCALE_FORMATS.len())?),
"placeholder" => F::PlaceholderProperty(index(codec::fields::BLOCK_PLACEHOLDER_PROPERTIES.len())?),
"placeholder" => F::PlaceholderProperty(index(
codec::fields::BLOCK_PLACEHOLDER_PROPERTIES.len(),
)?),
"table_function" => F::TableFunction(
["Average", "Sum", "Count", "Cell"]
.into_iter()
@@ -1362,15 +1496,22 @@ impl OpenCADStudio {
// "custom_owner=0|1", "add_custom_ok", "number=", "title=",
// "file_name=", "pick_layout=<i>", "ok", "cancel".
"sheet_set" => {
use crate::ui::window::sheet_set::{FieldId as F, MenuAction as A, SheetSetMsg as M, SsmTab, WizardMsg as W};
use crate::ui::window::sheet_set::{
FieldId as F, MenuAction as A, SheetSetMsg as M, SsmTab, WizardMsg as W,
};
let value = string(req, "value")?;
let (key, arg) = value.split_once('=').unwrap_or((value, ""));
let target = |app: &Self, spec: &str| {
app.sheet_set_target(spec).ok_or_else(|| failure("invalid_value", "No such sheet set component"))
app.sheet_set_target(spec)
.ok_or_else(|| failure("invalid_value", "No such sheet set component"))
};
let index_value = |spec: &str| -> Result<(usize, String), Value> {
let (i, v) = spec.split_once(':').unwrap_or((spec, ""));
Ok((i.parse().map_err(|_| failure("invalid_value", "Expected <index>:<value>"))?, v.to_string()))
Ok((
i.parse()
.map_err(|_| failure("invalid_value", "Expected <index>:<value>"))?,
v.to_string(),
))
};
Message::SheetSet(match key {
"open" => M::OpenPicked(Some(std::path::PathBuf::from(arg))),
@@ -1393,7 +1534,12 @@ impl OpenCADStudio {
"remove" => A::Remove,
"properties" => A::Properties,
"close" => A::Close,
_ => return Err(failure("invalid_value", "Unknown sheet set menu action")),
_ => {
return Err(failure(
"invalid_value",
"Unknown sheet set menu action",
))
}
};
if action == A::ImportLayout {
self.sheet_set_import_parent = Some(target(self, spec)?);
@@ -1403,7 +1549,9 @@ impl OpenCADStudio {
}
"new" => return Ok(self.dispatch_command("NEWSHEETSET")),
"existing" => M::Wizard(W::Existing(arg == "1")),
"step" => M::Wizard(W::Step(arg.parse().map_err(|_| failure("invalid_value", "step"))?)),
"step" => M::Wizard(W::Step(
arg.parse().map_err(|_| failure("invalid_value", "step"))?,
)),
"layout" => {
let (i, v) = index_value(arg)?;
M::Wizard(W::LayoutOn(i, v == "1"))
@@ -1412,7 +1560,9 @@ impl OpenCADStudio {
"description" => M::Input(F::Description, arg.into()),
"folder" => M::Input(F::Folder, arg.into()),
"hierarchy" => M::Toggle(F::Hierarchy, arg == "1"),
"add_folder" => M::FolderPicked(F::AddFolder, Some(std::path::PathBuf::from(arg))),
"add_folder" => {
M::FolderPicked(F::AddFolder, Some(std::path::PathBuf::from(arg)))
}
"wizard_props" => M::WizardProperties,
"row" => {
let (i, v) = index_value(arg)?;
@@ -1435,7 +1585,10 @@ impl OpenCADStudio {
"number" => M::Input(F::Number, arg.into()),
"title" => M::Input(F::Title, arg.into()),
"file_name" => M::Input(F::FileName, arg.into()),
"pick_layout" => M::Choose(F::Layout, arg.parse().map_err(|_| failure("invalid_value", "index"))?),
"pick_layout" => M::Choose(
F::Layout,
arg.parse().map_err(|_| failure("invalid_value", "index"))?,
),
"refresh" => M::Refresh,
"open_after" => M::Toggle(F::OpenAfter, arg == "1"),
"row_toggle" => {
@@ -1448,7 +1601,9 @@ impl OpenCADStudio {
let (i, v) = index_value(arg)?;
M::Toggle(F::RenameOption(i.min(3)), v == "1")
}
"import_add" => M::ImportAdd(arg.split('|').map(std::path::PathBuf::from).collect()),
"import_add" => {
M::ImportAdd(arg.split('|').map(std::path::PathBuf::from).collect())
}
"import_row" => {
let (i, v) = index_value(arg)?;
M::Toggle(F::ImportRow(i), v == "1")
@@ -1462,7 +1617,12 @@ impl OpenCADStudio {
let id = self
.sheet_set
.db()
.and_then(|db| db.view_categories().iter().find(|c| c.prop("Name") == Some(arg)).map(|c| c.id().to_string()))
.and_then(|db| {
db.view_categories()
.iter()
.find(|c| c.prop("Name") == Some(arg))
.map(|c| c.id().to_string())
})
.ok_or_else(|| failure("invalid_value", "No such view category"))?;
M::CategoryProperties(id)
}
@@ -1470,7 +1630,12 @@ impl OpenCADStudio {
let id = self
.sheet_set
.db()
.and_then(|db| db.view_categories().iter().find(|c| c.prop("Name") == Some(arg)).map(|c| c.id().to_string()))
.and_then(|db| {
db.view_categories()
.iter()
.find(|c| c.prop("Name") == Some(arg))
.map(|c| c.id().to_string())
})
.ok_or_else(|| failure("invalid_value", "No such view category"))?;
M::CategoryRemove(id)
}
@@ -1484,7 +1649,10 @@ impl OpenCADStudio {
.sheet_set
.locations
.iter()
.find(|(_, folder, _)| codec::sheet_set::path_key(folder) == codec::sheet_set::path_key(arg))
.find(|(_, folder, _)| {
codec::sheet_set::path_key(folder)
== codec::sheet_set::path_key(arg)
})
.map(|(id, _, _)| id.clone())
.ok_or_else(|| failure("invalid_value", "No such location"))?;
M::RemoveLocation(id)
@@ -1494,19 +1662,29 @@ impl OpenCADStudio {
"see_views" => M::SeeViews(arg.into()),
// place=<drawing>[|<view>]
"place" => {
let (drawing, view) = arg.split_once('|').map_or((arg, None), |(d, v)| (d, Some(v.to_string())));
let (drawing, view) = arg
.split_once('|')
.map_or((arg, None), |(d, v)| (d, Some(v.to_string())));
M::PlaceOnSheet(drawing.into(), view)
}
// view=<view title or number - title>:<display|rename|label|category=<name>|callout=<block>>
"view" => {
use crate::ui::window::sheet_set::ViewAction as VA;
let (spec, action) = arg.rsplit_once(':').ok_or_else(|| failure("invalid_value", "view=<view>:<action>"))?;
let db = self.sheet_set.db().ok_or_else(|| failure("invalid_state", "No sheet set"))?;
let (spec, action) = arg
.rsplit_once(':')
.ok_or_else(|| failure("invalid_value", "view=<view>:<action>"))?;
let db = self
.sheet_set
.db()
.ok_or_else(|| failure("invalid_state", "No sheet set"))?;
let view = db
.sheets()
.into_iter()
.flat_map(|s| db.sheet_views(s))
.find(|v| codec::sheet_set::number_and_title(v) == spec || v.prop("Title") == Some(spec))
.find(|v| {
codec::sheet_set::number_and_title(v) == spec
|| v.prop("Title") == Some(spec)
})
.map(|v| v.id().to_string())
.ok_or_else(|| failure("invalid_value", "No such sheet view"))?;
let (verb, name) = action.split_once('=').unwrap_or((action, ""));
@@ -1519,24 +1697,37 @@ impl OpenCADStudio {
.iter()
.find(|c| c.prop("Name") == Some(name))
.map(|c| c.id().to_string())
.ok_or_else(|| failure("invalid_value", "No such view category"))?,
.ok_or_else(|| {
failure("invalid_value", "No such view category")
})?,
),
"callout" => VA::Callout(
db.callout_blocks()
.iter()
.find(|b| b.prop("Name") == Some(name))
.map(|b| b.id().to_string())
.ok_or_else(|| failure("invalid_value", "No such callout block"))?,
.ok_or_else(|| {
failure("invalid_value", "No such callout block")
})?,
),
_ => return Err(failure("invalid_value", "display|rename|label|category=|callout=")),
_ => {
return Err(failure(
"invalid_value",
"display|rename|label|category=|callout=",
))
}
};
M::ViewMenu(view, a)
}
"add_blocks" => M::AddBlocks,
"block_list_select" => M::BlockListSelect(arg.parse().map_err(|_| failure("invalid_value", "index"))?),
"block_list_select" => M::BlockListSelect(
arg.parse().map_err(|_| failure("invalid_value", "index"))?,
),
"block_list_add" => M::BlockListAdd,
"block_list_delete" => M::BlockListDelete,
"select_block_file" => M::SelectBlockPicked(Some(std::path::PathBuf::from(arg))),
"select_block_file" => {
M::SelectBlockPicked(Some(std::path::PathBuf::from(arg)))
}
"select_block_whole" => M::SelectBlockWhole(arg == "1"),
"select_block_check" => {
let (i, v) = index_value(arg)?;
@@ -1563,22 +1754,22 @@ impl OpenCADStudio {
}
// The open dialog's OK button.
"dialog_ok" => match self.active_modal {
Some(crate::app::ModalKind::XrefAttach) => Message::XrefAttach(
crate::ui::window::xref_attach::XrefAttachMsg::Apply,
),
Some(crate::app::ModalKind::XrefAttach) => {
Message::XrefAttach(crate::ui::window::xref_attach::XrefAttachMsg::Apply)
}
Some(crate::app::ModalKind::BlockDefinition) => Message::BlockDefApply,
Some(crate::app::ModalKind::AttDef) => Message::AttdefDialog(
crate::ui::window::attdef_dialog::AttdefDialogMsg::Ok,
),
Some(crate::app::ModalKind::AttDefEdit) => Message::AttdefDialog(
crate::ui::window::attdef_dialog::AttdefDialogMsg::EditOk,
),
Some(crate::app::ModalKind::SheetSet) => Message::SheetSet(
crate::ui::window::sheet_set::SheetSetMsg::Ok,
),
Some(crate::app::ModalKind::Field) => Message::FieldDialog(
crate::ui::window::field_dialog::FieldDialogMsg::Ok,
),
Some(crate::app::ModalKind::AttDef) => {
Message::AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg::Ok)
}
Some(crate::app::ModalKind::AttDefEdit) => {
Message::AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg::EditOk)
}
Some(crate::app::ModalKind::SheetSet) => {
Message::SheetSet(crate::ui::window::sheet_set::SheetSetMsg::Ok)
}
Some(crate::app::ModalKind::Field) => {
Message::FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg::Ok)
}
_ => return Err(failure("no_dialog", "No dialog with an OK button is open")),
},
"close_document" => Message::TabClose(self.tabs[self.active_tab].id),
@@ -1597,7 +1788,10 @@ impl OpenCADStudio {
"mtext_cancel" => Message::MTextCancel,
"text_input" => Message::TextInlineInput(string(req, "value")?.into()),
"text_commit" => Message::TextInlineOk,
"pointer_move" | "pointer_press" | "pointer_release" | "pointer_right_press"
"pointer_move"
| "pointer_press"
| "pointer_release"
| "pointer_right_press"
| "pointer_right_release" => {
let x = req["x"]
.as_f64()
@@ -1609,7 +1803,12 @@ impl OpenCADStudio {
.filter(|v| v.is_finite())
.ok_or_else(|| failure("invalid_point", "Missing finite y"))?
as f32;
let size = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size;
let size = self.tabs[self.active_tab]
.scene
.selection
.borrow()
.view
.vp_size;
if x < 0. || y < 0. || x > size.0 || y > size.1 {
return Err(failure(
"outside_viewport",
+31 -10
View File
@@ -20,7 +20,11 @@ fn point_field(req: &Value, key: &str) -> Parsed<codec::types::Vector3> {
.filter(|v| v.is_finite())
.ok_or_else(|| failure("invalid_point", format!("{key} needs finite coordinates")))
};
Ok(codec::types::Vector3::new(coord(0)?, coord(1)?, values.get(2).and_then(Value::as_f64).unwrap_or(0.0)))
Ok(codec::types::Vector3::new(
coord(0)?,
coord(1)?,
values.get(2).and_then(Value::as_f64).unwrap_or(0.0),
))
}
fn deg_field(req: &Value, key: &str) -> f64 {
@@ -31,7 +35,12 @@ pub(super) fn hex_handles(req: &Value, key: &str) -> Parsed<Vec<codec::Handle>>
let list = req[key]
.as_array()
.filter(|v| !v.is_empty())
.ok_or_else(|| failure("handles_required", format!("Supply a non-empty {key}:[…] list")))?;
.ok_or_else(|| {
failure(
"handles_required",
format!("Supply a non-empty {key}:[…] list"),
)
})?;
list.iter()
.map(|v| {
u64::from_str_radix(
@@ -54,7 +63,10 @@ pub(super) fn hex_handles(req: &Value, key: &str) -> Parsed<Vec<codec::Handle>>
/// Every handle must exist in the document; otherwise `entity_absent` naming
/// the missing ones so the caller can refresh instead of guessing.
pub(super) fn require_existing(document: &codec::CadDocument, handles: &[codec::Handle]) -> Parsed<()> {
pub(super) fn require_existing(
document: &codec::CadDocument,
handles: &[codec::Handle],
) -> Parsed<()> {
let missing: Vec<String> = handles
.iter()
.filter(|h| document.get_entity(**h).is_none())
@@ -560,7 +572,11 @@ impl OpenCADStudio {
.map(|record| {
(
record.application_name.clone(),
json!(record.values.iter().map(xdata_value_json).collect::<Vec<_>>()),
json!(record
.values
.iter()
.map(xdata_value_json)
.collect::<Vec<_>>()),
)
})
.collect();
@@ -594,11 +610,10 @@ impl OpenCADStudio {
// World → block space translates `base` onto the block origin; the
// inverse places the Insert so entities keep their world position.
let world_to_block = codec::types::Transform::from_translation(
codec::types::Vector3::new(-base.x, -base.y, -base.z),
);
let block_to_world =
codec::types::Transform::from_translation(insert_at);
let world_to_block = codec::types::Transform::from_translation(codec::types::Vector3::new(
-base.x, -base.y, -base.z,
));
let block_to_world = codec::types::Transform::from_translation(insert_at);
self.push_undo_snapshot(i, "BLOCKDEFINE");
// CF-01.3 (barcode re-import): `replace:true` drops the previous
// definition — its children, markers and inserts — inside the same
@@ -629,7 +644,13 @@ impl OpenCADStudio {
));
}
let i = self.active_tab;
if self.tabs[i].scene.document.block_records.get(&name).is_none() {
if self.tabs[i]
.scene
.document
.block_records
.get(&name)
.is_none()
{
return Err(failure(
"block_missing",
format!("Block '{name}' does not exist"),
+37 -16
View File
@@ -19,7 +19,7 @@ use iced::futures::{channel::mpsc, Stream};
use serde_json::{json, Value};
use std::io::ErrorKind;
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicU64, AtomicU16, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicU16, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -111,7 +111,10 @@ fn forward(
let path = if path.is_empty() { "/" } else { path };
let segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
if segments.first() != Some(&"api") || segments.get(1) != Some(&"v1") {
return (404, json!({"ok": false, "code": "unknown_route", "error": "Use /api/v1/…"}));
return (
404,
json!({"ok": false, "code": "unknown_route", "error": "Use /api/v1/…"}),
);
}
let plan = rest::plan(&request.method, &segments[2..], request);
// The picks park until the person answers; an `operation` call is itself
@@ -126,20 +129,27 @@ fn forward(
Plan::Ready => {
let state = transact(sender, json!({"protocol":1,"op":"state"}), wait, stream);
harvest(document_cache, &state);
(200, json!({
"ok": true,
"ready": true,
"version": state["version"],
"session_id": state["session_id"],
"document_id": state["document_id"],
}))
(
200,
json!({
"ok": true,
"ready": true,
"version": state["version"],
"session_id": state["session_id"],
"document_id": state["document_id"],
}),
)
}
Plan::Run { request, created } => {
let response = transact(sender, prepare(request), wait, stream);
harvest(document_cache, &response);
(rest::map_status(response.clone(), created == 201), response)
}
Plan::Mutate { op, fields, created } => {
Plan::Mutate {
op,
fields,
created,
} => {
let cached = *document_cache.lock().unwrap();
let mut response = transact(sender, mutate_request(&op, &fields, cached), wait, stream);
let code = response["code"].as_str().unwrap_or("");
@@ -153,7 +163,10 @@ fn forward(
}
harvest(document_cache, &response);
let status = rest::map_status(response.clone(), created == 201);
(if status == 200 { created } else { status }, compact(response))
(
if status == 200 { created } else { status },
compact(response),
)
}
}
}
@@ -207,7 +220,9 @@ fn transact(
&& std::time::Instant::now() < deadline
{
if interactive && peer_gone(stream) {
eprintln!("[pick] {op} {request_id}: client disconnected -- cancelling the pending pick");
eprintln!(
"[pick] {op} {request_id}: client disconnected -- cancelling the pending pick"
);
// The cancel reuses the pick's own request id: if the person
// answered in the race window, the settled id makes the retract
// a harmless no-op instead of an Escape into the live session.
@@ -436,7 +451,10 @@ mod tests {
assert_eq!(body["status"], "completed");
assert_eq!(body["result"]["count"], 1);
assert_eq!(body["result"]["entities"][0]["handle"], handle.as_str());
assert!(body["result"]["entities"][0]["bounds"].is_object(), "{body}");
assert!(
body["result"]["entities"][0]["bounds"].is_object(),
"{body}"
);
assert_eq!(body["result"]["entities"][0]["type"], "Line");
// Unrouted paths are refused locally; the GUI is never woken for
@@ -508,8 +526,7 @@ mod tests {
// until the person answers. Simulate the click right away.
assert_eq!(reply["status"], "accepted", "{reply}");
envelope.reply.send(reply);
app.tabs[app.active_tab].last_cursor_world =
glam::DVec3::new(1.5, 2.5, 0.0);
app.tabs[app.active_tab].last_cursor_world = glam::DVec3::new(1.5, 2.5, 0.0);
let _ = app.update(crate::app::Message::ViewportLeftPress);
} else if envelope.request["op"] == "get_selection" {
envelope.reply.send(reply);
@@ -746,7 +763,11 @@ mod tests {
let mut query_conn = get(port, "/api/v1/entities?type=Line&detail=full");
drain(&mut app, &mut receiver, 1);
let (_, body) = read_response(&mut query_conn);
assert_eq!(body["entities"][0]["start"], json!([5.0, 0.0, 0.0]), "{body}");
assert_eq!(
body["entities"][0]["start"],
json!([5.0, 0.0, 0.0]),
"{body}"
);
// A stale cache (the person switched documents) costs one state
// refresh and a retry, not an error.
+20 -22
View File
@@ -64,19 +64,19 @@ impl OpenCADStudio {
.filter(|value| !value.trim().is_empty())
.map(str::to_owned)
.unwrap_or_else(|| {
crate::t!("Select objects, then press Enter to finish (Esc cancels).")
.into_owned()
crate::t!("Select objects, then press Enter to finish (Esc cancels).").into_owned()
});
// Label the request on screen: the message list must say WHO is
// waiting and for what, and the line stays pinned (non-fading) for
// as long as the request is open — see `pending_pick_label`.
let request_id = req["request_id"]
.as_str()
.unwrap_or_default()
.to_owned();
let request_id = req["request_id"].as_str().unwrap_or_default().to_owned();
let label = crate::tf!(
"[user_select · {}] {} ({})",
if request_id.is_empty() { "client" } else { &request_id },
if request_id.is_empty() {
"client"
} else {
&request_id
},
prompt,
crate::t!("Enter confirms, Esc cancels"),
)
@@ -107,8 +107,7 @@ impl OpenCADStudio {
return;
};
if !confirmed {
self.command_line
.push_info(crate::t!("*Cancel*").as_ref());
self.command_line.push_info(crate::t!("*Cancel*").as_ref());
self.command_line.push_info(
crate::tf!(
"user_select {}: the request was answered with no objects.",
@@ -256,13 +255,14 @@ impl OpenCADStudio {
.filter(|value| !value.trim().is_empty())
.map(str::to_owned)
.unwrap_or_else(|| crate::t!("Pick a point, then click (Esc cancels).").into_owned());
let request_id = req["request_id"]
.as_str()
.unwrap_or_default()
.to_owned();
let request_id = req["request_id"].as_str().unwrap_or_default().to_owned();
let label = crate::tf!(
"[getpoint · {}] {} ({})",
if request_id.is_empty() { "client" } else { &request_id },
if request_id.is_empty() {
"client"
} else {
&request_id
},
prompt,
crate::t!("Click confirms, Esc cancels"),
)
@@ -288,7 +288,11 @@ impl OpenCADStudio {
};
// Drop the snap marker the pending pick was showing — no command is
// running, so nothing else would clear it after the session ends.
if let Some(i) = self.tabs.iter().position(|tab| tab.id == session.document_id) {
if let Some(i) = self
.tabs
.iter()
.position(|tab| tab.id == session.document_id)
{
self.tabs[i].snap_result = None;
}
let (cancelled, result) = match point {
@@ -339,13 +343,7 @@ impl OpenCADStudio {
/// settle. `cancelled` flips the final status to "cancelled". The
/// request_id + op check keeps a stale answer from writing into someone
/// else's result slot.
fn finish_interactive(
&mut self,
op: &str,
request_id: &str,
cancelled: bool,
result: Value,
) {
fn finish_interactive(&mut self, op: &str, request_id: &str, cancelled: bool, result: Value) {
if let Some(pending) = self
.control
.pending
+118 -74
View File
@@ -2,7 +2,7 @@
use super::{Message, OpenCADStudio};
use crate::command::{InputKind, StepInput};
use iced::Task;
use serde_json::{Value, json};
use serde_json::{json, Value};
use std::{collections::VecDeque, sync::OnceLock};
#[cfg(not(target_arch = "wasm32"))]
mod transport;
@@ -679,7 +679,10 @@ impl OpenCADStudio {
);
}
if self.tabs[self.active_tab].id != id
&& !matches!(op, "activate" | "switch_document" | "close" | "entities_copy_to")
&& !matches!(
op,
"activate" | "switch_document" | "close" | "entities_copy_to"
)
{
return (
failure(
@@ -689,19 +692,14 @@ impl OpenCADStudio {
Task::none(),
);
}
// Interactive requests operate on the active tab by definition, so
// a client may omit document_id (the GUI-hosted REST channel does).
} else if !query
&& !matches!(
op,
"new" | "open" | "stop" | "user_select" | "getpoint"
)
{
return (
failure("document_required", "Read state and supply document_id"),
Task::none(),
);
}
// Interactive requests operate on the active tab by definition, so
// a client may omit document_id (the GUI-hosted REST channel does).
} else if !query && !matches!(op, "new" | "open" | "stop" | "user_select" | "getpoint") {
return (
failure("document_required", "Read state and supply document_id"),
Task::none(),
);
}
let tab = &self.tabs[self.active_tab];
if req["revision"]
.as_u64()
@@ -739,7 +737,9 @@ impl OpenCADStudio {
});
let i = self.active_tab;
match self.snap_query(i, world, from) {
Some(hit) => json!({"ok":true,"snap":format!("{:?}", hit.snap_type),"world":[hit.world.x,hit.world.y,hit.world.z]}),
Some(hit) => {
json!({"ok":true,"snap":format!("{:?}", hit.snap_type),"world":[hit.world.x,hit.world.y,hit.world.z]})
}
None => json!({"ok":true,"snap":null}),
}
}
@@ -758,7 +758,10 @@ impl OpenCADStudio {
let client = req["client_id"].as_str().unwrap_or("default").to_owned();
if tab.active_cmd.is_some()
&& (self.control.owner.as_ref() != Some(&(tab.id, client.clone()))
|| matches!(op, "run" | "start" | "new" | "open" | "activate" | "switch_document"))
|| matches!(
op,
"run" | "start" | "new" | "open" | "activate" | "switch_document"
))
&& op != "cancel"
{
return (
@@ -1030,11 +1033,8 @@ impl OpenCADStudio {
Some(&path),
)
.map_err(|error| failure("invalid_target", error))?;
let dropped = crate::io::dropped_on_save_count(
&self.tabs[i].scene.document,
version,
is_dxf,
);
let dropped =
crate::io::dropped_on_save_count(&self.tabs[i].scene.document, version, is_dxf);
if dropped > 0 && req["allow_lossy"].as_bool() != Some(true) {
return Err(failure(
"lossy_conversion_not_acknowledged",
@@ -1104,7 +1104,9 @@ impl OpenCADStudio {
.map(codec::Handle::new)
.collect();
if !hex_handles.is_empty() {
self.tabs[self.active_tab].scene.zoom_to_entities(&hex_handles);
self.tabs[self.active_tab]
.scene
.zoom_to_entities(&hex_handles);
}
} else if let Some(bounds) = req.get("bounds").and_then(Value::as_array) {
if bounds.len() >= 4 {
@@ -1298,7 +1300,8 @@ impl OpenCADStudio {
let Some(entity) = tab.scene.document.get_entity(handle) else {
continue;
};
let (min, max) = crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, entity);
let (min, max) =
crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, entity);
let metrics = tab
.scene
.meshes
@@ -1362,9 +1365,22 @@ impl OpenCADStudio {
.ok_or("Renderer returned malformed image data")?;
let mut actual_scope = "window";
let (vp_w, vp_h) = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size;
let mut vp_logical_width = if vp_w > 0.0 { vp_w } else { image.width() as f32 / s.scale_factor };
let mut vp_logical_height = if vp_h > 0.0 { vp_h } else { image.height() as f32 / s.scale_factor };
let (vp_w, vp_h) = self.tabs[self.active_tab]
.scene
.selection
.borrow()
.view
.vp_size;
let mut vp_logical_width = if vp_w > 0.0 {
vp_w
} else {
image.width() as f32 / s.scale_factor
};
let mut vp_logical_height = if vp_h > 0.0 {
vp_h
} else {
image.height() as f32 / s.scale_factor
};
if requested_scope == "viewport" {
if let Some(bounds) = crate::ui::wrap_bar::dropdown_bounds(
@@ -1444,7 +1460,11 @@ impl OpenCADStudio {
.map_err(|e| e.to_string())?;
let doc_unit = crate::app::properties::insunits_name(
self.tabs[self.active_tab].scene.document.header.insertion_units,
self.tabs[self.active_tab]
.scene
.document
.header
.insertion_units,
);
let spatial_obj = json!({
@@ -1488,8 +1508,8 @@ impl OpenCADStudio {
}
}
}
pub(crate) mod vision;
mod actions;
pub(crate) mod vision;
pub(crate) use actions::property_json;
mod entities;
pub(crate) use entities::erase_block_definition;
@@ -1498,8 +1518,8 @@ pub(crate) mod http_bridge;
mod interactive;
mod sheets;
pub(crate) use sheets::new_guid_v4;
pub(crate) mod text_ops;
pub(crate) mod spellcheck;
pub(crate) mod text_ops;
#[cfg(test)]
mod p1_tests;
@@ -1537,12 +1557,10 @@ mod tests {
);
let started = request(&mut app, json!({"op":"start","cmd":"LINE"}));
assert_eq!(started["status"], "waiting_input");
assert!(
started["state"]["command"]["accepts"]
.as_array()
.unwrap()
.contains(&json!("point"))
);
assert!(started["state"]["command"]["accepts"]
.as_array()
.unwrap()
.contains(&json!("point")));
assert_eq!(
started["state"]["command"]["input_example"]["kind"],
"point"
@@ -1567,12 +1585,10 @@ mod tests {
);
assert_eq!(app.automation_op(r#"{"op":"entities"}"#)["total"], 1);
request(&mut app, json!({"op":"select","type":"LINE"}));
assert!(
!app.control_properties()["sections"]
.as_array()
.unwrap()
.is_empty()
);
assert!(!app.control_properties()["sections"]
.as_array()
.unwrap()
.is_empty());
let changed = request(&mut app, json!({"op":"property","field":"color","value":1}));
assert_eq!(changed["ok"], true, "{changed}");
assert_eq!(
@@ -1907,8 +1923,11 @@ mod tests {
assert_eq!(ole_count(&app), 1);
// The entity must be queryable like any other and undoable as one step.
let queried =
app.automation_op(json!({"op":"entities","type":"OLE2FRAME"}).to_string().as_str());
let queried = app.automation_op(
json!({"op":"entities","type":"OLE2FRAME"})
.to_string()
.as_str(),
);
assert_eq!(queried["total"], 1, "{queried}");
request(&mut app, json!({"op":"undo"}));
assert_eq!(ole_count(&app), 0);
@@ -1969,7 +1988,10 @@ mod tests {
fn user_select_stays_running_until_the_user_confirms_then_returns_entities() {
let mut app = OpenCADStudio::new_for_test();
app.main_window = Some(iced::window::Id::unique());
assert_eq!(request(&mut app, json!({"op":"new"}))["status"], "completed");
assert_eq!(
request(&mut app, json!({"op":"new"}))["status"],
"completed"
);
request(&mut app, json!({"op":"run","cmd":"LINE 0,0 10,10"}));
let asked = request(
&mut app,
@@ -2144,7 +2166,7 @@ mod tests {
let tab = &mut app.tabs[i];
tab.scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
tab.scene.sync_tiles_from_panes(1920.0, 1080.0);
tab.scene.fit_all(); // bring the line into the pane so snaps can hit
tab.scene.fit_all(); // bring the line into the pane so snaps can hit
}
// Park a client pick: with no command running, a cursor move must
// still run the object-snap engine so the marker shows and the
@@ -2174,8 +2196,7 @@ mod tests {
)
};
let _ = app.update(Message::ViewportMove(iced::Point::new(
sx as f32,
sy as f32,
sx as f32, sy as f32,
)));
assert!(
@@ -2232,8 +2253,7 @@ mod tests {
)
};
let _ = app.update(Message::ViewportMove(iced::Point::new(
sx as f32,
sy as f32,
sx as f32, sy as f32,
)));
assert!(
app.tabs[i].snap_result.is_some(),
@@ -2248,7 +2268,9 @@ mod tests {
.unwrap()
.to_owned();
let value = u64::from_str_radix(&handle, 16).unwrap();
app.tabs[i].scene.select_entity(codec::Handle::new(value), false);
app.tabs[i]
.scene
.select_entity(codec::Handle::new(value), false);
let _ = app.update(Message::CommandFinalize);
let done = user_select_result(&mut app, "us-snap");
assert_eq!(done["status"], "completed", "{done}");
@@ -2290,7 +2312,9 @@ mod tests {
json!({"op":"entities_create","entities":[{"type":"Circle","center":[0,0],"radius":2}]}),
);
let circle_handle = circle["result"]["handles"][0].as_str().unwrap().to_owned();
app.automation_op(&format!(r#"{{"op":"select","handles":["{circle_handle}"]}}"#));
app.automation_op(&format!(
r#"{{"op":"select","handles":["{circle_handle}"]}}"#
));
let read = request(&mut app, json!({"op":"get_selection"}));
assert_eq!(read["result"]["count"], 1);
assert_eq!(read["result"]["entities"][0]["block"], Value::Null);
@@ -2330,7 +2354,8 @@ mod tests {
// The plotted PDF carries two distinct pen widths — the per-layer
// 0.13 mm / 0.50 mm hierarchy survives the plot style (thin clamps to
// the 1 px floor, so the ratio is the clamped-pixel ratio 1.889).
let text = crate::io::pdf_export::pdf_stream_text(&std::fs::read(&pdf).expect("pdf written"));
let text =
crate::io::pdf_export::pdf_stream_text(&std::fs::read(&pdf).expect("pdf written"));
let tokens: Vec<&str> = text.split_whitespace().collect();
let mut widths: Vec<f32> = tokens
.windows(2)
@@ -2345,7 +2370,11 @@ mod tests {
widths.sort_by(|a, b| a.partial_cmp(b).unwrap());
widths.dedup_by(|a, b| (*a - *b).abs() < 0.01);
widths.retain(|width| *width > 0.2);
assert_eq!(widths.len(), 2, "two layer weights in the plotted PDF: {widths:?}");
assert_eq!(
widths.len(),
2,
"two layer weights in the plotted PDF: {widths:?}"
);
assert!(
(widths[1] / widths[0] - 1.889).abs() < 0.15,
"0.50 mm vs 0.13 mm hierarchy: {widths:?}"
@@ -2391,7 +2420,8 @@ mod tests {
);
assert_eq!(response["status"], "completed", "{response}");
let text = crate::io::pdf_export::pdf_stream_text(&std::fs::read(&pdf).expect("pdf written"));
let text =
crate::io::pdf_export::pdf_stream_text(&std::fs::read(&pdf).expect("pdf written"));
// Monochrome maps every index colour (ByLayer cyan frame, explicit
// red) to black; the true-color line keeps its RGB by definition.
assert!(
@@ -2413,10 +2443,15 @@ mod tests {
// Without a plot style the same drawing keeps its colours: the cyan
// frame plots as the dark-blue print remap, the red line stays red.
let plain = std::env::temp_dir().join(format!("ocs-plot-aci-plain-{}.pdf", std::process::id()));
let response = request(&mut app, json!({"op":"plot","path":plain.to_string_lossy()}));
let plain =
std::env::temp_dir().join(format!("ocs-plot-aci-plain-{}.pdf", std::process::id()));
let response = request(
&mut app,
json!({"op":"plot","path":plain.to_string_lossy()}),
);
assert_eq!(response["status"], "completed", "{response}");
let text = crate::io::pdf_export::pdf_stream_text(&std::fs::read(&plain).expect("pdf written"));
let text =
crate::io::pdf_export::pdf_stream_text(&std::fs::read(&plain).expect("pdf written"));
let color_ops: Vec<[f32; 3]> = {
let tokens: Vec<&str> = text.split_whitespace().collect();
tokens
@@ -2470,28 +2505,37 @@ mod tests {
let handle = lines["entities"][0]["handle"].as_str().unwrap().to_string();
// Not one of the six whitelisted actions.
let rejected = request(&mut app, json!({
"op":"entities_transform",
"handles":[handle],
"action":"explode"
}));
let rejected = request(
&mut app,
json!({
"op":"entities_transform",
"handles":[handle],
"action":"explode"
}),
);
assert_eq!(rejected["code"], "invalid_action", "{rejected}");
// Whitespace-only is unknown too — not a panic.
let blank = request(&mut app, json!({
"op":"entities_transform",
"handles":[handle],
"action":" "
}));
let blank = request(
&mut app,
json!({
"op":"entities_transform",
"handles":[handle],
"action":" "
}),
);
assert_eq!(blank["code"], "invalid_action", "{blank}");
// A valid action is matched case- and whitespace-insensitively.
let moved = request(&mut app, json!({
"op":"entities_transform",
"handles":[handle],
"action":" move ",
"vector":[5,0,0]
}));
let moved = request(
&mut app,
json!({
"op":"entities_transform",
"handles":[handle],
"action":" move ",
"vector":[5,0,0]
}),
);
assert_eq!(moved["ok"], true, "{moved}");
}
}
+63 -20
View File
@@ -69,12 +69,14 @@ fn close_op_guards_dirty_documents_and_discards() {
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert_eq!(r["result"]["closed"], true);
assert_eq!(app.tabs[app.active_tab].scene.document.entities().count(), 0);
assert_eq!(
app.tabs[app.active_tab].scene.document.entities().count(),
0
);
// Unknown document id: document_closed.
let r = app.automation_op(
r#"{"protocol":1,"op":"close","request_id":"cl2","document_id":9999}"#,
);
let r =
app.automation_op(r#"{"protocol":1,"op":"close","request_id":"cl2","document_id":9999}"#);
assert_eq!(r["ok"], false);
assert_eq!(r["code"], "document_closed");
}
@@ -109,7 +111,11 @@ fn sysvar_round_trips_and_refuses_unknowns() {
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert_eq!(
app.tabs[app.active_tab].scene.document.header.current_layer_name,
app.tabs[app.active_tab]
.scene
.document
.header
.current_layer_name,
"Walls"
);
let r = app.automation_op(&format!(
@@ -135,7 +141,11 @@ fn layout_create_and_page_setup_configure_sheets() {
r#"{{"protocol":1,"op":"layout_create","request_id":"lc1","document_id":{doc},"name":"PLAN"}}"#
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert!(r["result"]["layouts"].as_array().unwrap().iter().any(|n| n == "PLAN"));
assert!(r["result"]["layouts"]
.as_array()
.unwrap()
.iter()
.any(|n| n == "PLAN"));
// Duplicate refused.
let r = app.automation_op(&format!(
@@ -222,7 +232,10 @@ fn wblock_with_template_base_keeps_template_tables() {
let tpl = std::env::temp_dir().join(format!("ocs_wbtpl_{}.dwg", std::process::id()));
let _ = std::fs::remove_file(&tpl);
let tp = tpl.to_string_lossy().replace('\\', "\\\\");
assert_eq!(app.automation_op(&format!(r#"{{"op":"save","path":"{tp}"}}"#))["ok"], true);
assert_eq!(
app.automation_op(&format!(r#"{{"op":"save","path":"{tp}"}}"#))["ok"],
true
);
// A new drawing holds the one entity to export.
app.automation_op(r#"{"op":"new"}"#);
@@ -405,8 +418,8 @@ fn diagnostic_layer_survives_plain_io_round_trip() {
// Direct io round trip on a clone of the document.
let doc = app.tabs[app.active_tab].scene.document.clone();
let bytes = crate::io::save_to_bytes(&doc, "dxf", codec::DxfVersion::AC1032)
.expect("save to bytes");
let bytes =
crate::io::save_to_bytes(&doc, "dxf", codec::DxfVersion::AC1032).expect("save to bytes");
let path = std::env::temp_dir().join(format!("ocs_diag_{}.dxf", std::process::id()));
std::fs::write(&path, &bytes).unwrap();
let loaded = crate::io::load_file(&path).expect("load back");
@@ -453,7 +466,11 @@ fn block_delete_and_define_replace_manage_definitions() {
r#"{{"protocol":1,"op":"block_define","request_id":"bl1","document_id":{doc},"name":"MARK","base":[0,0,0],"handles":["{handle}"],"replace":true}}"#
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert_eq!(count_type(&mut app, "Insert"), 1, "one insert after replace");
assert_eq!(
count_type(&mut app, "Insert"),
1,
"one insert after replace"
);
assert_eq!(count_type(&mut app, "Line"), 0, "old children erased");
assert_eq!(count_type(&mut app, "Circle"), 0, "old children erased");
@@ -462,7 +479,10 @@ fn block_delete_and_define_replace_manage_definitions() {
r#"{{"protocol":1,"op":"block_delete","request_id":"bl2","document_id":{doc},"name":"MARK"}}"#
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert!(r["result"]["erased"].as_u64().unwrap() >= 3, "insert + markers + child");
assert!(
r["result"]["erased"].as_u64().unwrap() >= 3,
"insert + markers + child"
);
assert_eq!(count_type(&mut app, "Insert"), 0);
// Unknown names and protected layout records are refused.
@@ -498,7 +518,10 @@ fn wblock_normalize_shifts_the_export_to_the_origin() {
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert_eq!(r["result"]["normalized"], true);
assert_eq!(app.automation_op(&format!(r#"{{"op":"open","path":"{op}"}}"#))["ok"], true);
assert_eq!(
app.automation_op(&format!(r#"{{"op":"open","path":"{op}"}}"#))["ok"],
true
);
let q = app.automation_op(r#"{"op":"query","type":"Line","detail":"full"}"#);
assert_eq!(q["entities"][0]["start"], json!([0.0, 0.0, 0.0]));
assert_eq!(q["entities"][0]["end"], json!([10.0, 10.0, 0.0]));
@@ -539,8 +562,14 @@ fn file_identity_is_stable_and_survives_a_round_trip() {
let path = std::env::temp_dir().join(format!("ocs_fid_{}.dwg", std::process::id()));
let _ = std::fs::remove_file(&path);
let p = path.to_string_lossy().replace('\\', "\\\\");
assert_eq!(app.automation_op(&format!(r#"{{"op":"save","path":"{p}"}}"#))["ok"], true);
assert_eq!(app.automation_op(&format!(r#"{{"op":"open","path":"{p}"}}"#))["ok"], true);
assert_eq!(
app.automation_op(&format!(r#"{{"op":"save","path":"{p}"}}"#))["ok"],
true
);
assert_eq!(
app.automation_op(&format!(r#"{{"op":"open","path":"{p}"}}"#))["ok"],
true
);
let r4 = mutate(
&mut app,
r#"{"protocol":1,"op":"file_identity","request_id":"fi4","document_id":{doc}}"#,
@@ -636,8 +665,14 @@ fn text_replace_op_batch_replaces_preserves_formatting_and_undoes() {
assert_eq!(q["ok"], true);
assert_eq!(q["count"], 1);
let raw = q["matches"][0]["raw_value"].as_str().unwrap();
assert!(raw.contains("Century Gothic"), "raw MText should keep font format code: {raw}");
assert!(raw.contains("NON TRAITÉE"), "raw MText should contain replaced text: {raw}");
assert!(
raw.contains("Century Gothic"),
"raw MText should keep font format code: {raw}"
);
assert!(
raw.contains("NON TRAITÉE"),
"raw MText should contain replaced text: {raw}"
);
// Verify Text was replaced
let q2 = app.automation_op(r#"{"op":"text_search","find":"VANNE D'ARRÊT"}"#);
@@ -692,7 +727,10 @@ fn text_audit_op_simulates_dry_run_checks_terms_and_dictionary() {
// Unmatched reporting
assert_eq!(audit_res["unmatched_pairs"], json!(["INEXISTANT_PAIR"]));
assert_eq!(audit_res["unmatched_check_terms"], json!(["INEXISTANT_TERM"]));
assert_eq!(
audit_res["unmatched_check_terms"],
json!(["INEXISTANT_TERM"])
);
// Simulated changes has 2 entries (Text and MText)
let changes = audit_res["simulated_changes"].as_array().unwrap();
@@ -700,7 +738,10 @@ fn text_audit_op_simulates_dry_run_checks_terms_and_dictionary() {
assert_eq!(changes[0]["find"], "CIRTUITS");
assert_eq!(changes[0]["replace"], "CIRCUITS");
assert_eq!(changes[0]["replaced"], 1);
assert!(changes[1]["after"].as_str().unwrap().contains("NON TRAITÉE"));
assert!(changes[1]["after"]
.as_str()
.unwrap()
.contains("NON TRAITÉE"));
// Suspect matches
let suspects = audit_res["suspect_matches"].as_array().unwrap();
@@ -835,7 +876,10 @@ fn text_audit_system_spellcheck_flags_misspelled_words() {
// Summary should reflect unrecognized words
assert!(
audit["summary"]["unrecognized_words_count"].as_u64().unwrap() >= 2,
audit["summary"]["unrecognized_words_count"]
.as_u64()
.unwrap()
>= 2,
"expected at least 2 unrecognized words in summary"
);
} else {
@@ -848,4 +892,3 @@ fn text_audit_system_spellcheck_flags_misspelled_words() {
);
}
}
+53 -47
View File
@@ -53,11 +53,12 @@ impl OpenCADStudio {
self.push_undo_snapshot(self.active_tab, "SYSVAR");
let mut applied = serde_json::Map::new();
for (name, value) in set {
set_sysvar(&mut self.tabs[self.active_tab].scene.document, name, value)
.map_err(|e| {
set_sysvar(&mut self.tabs[self.active_tab].scene.document, name, value).map_err(
|e| {
// Nothing committed yet — the snapshot is empty.
e
})?;
},
)?;
applied.insert(name.to_ascii_lowercase(), value.clone());
}
self.set_control_result(json!({ "set": applied }));
@@ -92,16 +93,18 @@ impl OpenCADStudio {
}
/// `layout_create` — add a layout with the default A4 page setup.
pub(super) fn control_layout_create(
&mut self,
req: &Value,
) -> Result<Task<Message>, Value> {
pub(super) fn control_layout_create(&mut self, req: &Value) -> Result<Task<Message>, Value> {
let name = string(req, "name")?.to_owned();
if name.is_empty() {
return Err(failure("name_required", "Supply a layout name"));
}
let i = self.active_tab;
if self.tabs[i].scene.layout_names().iter().any(|n| n.eq_ignore_ascii_case(&name)) {
if self.tabs[i]
.scene
.layout_names()
.iter()
.any(|n| n.eq_ignore_ascii_case(&name))
{
return Err(failure(
"layout_exists",
format!("Layout '{name}' already exists"),
@@ -118,9 +121,14 @@ impl OpenCADStudio {
.as_ref()
.map(|style| style.name.clone())
.unwrap_or_default();
let layout_flags = i16::from(
self.tabs[i].scene.document.header.paper_space_linetype_scaling,
) | (i16::from(self.tabs[i].scene.document.header.paper_space_limit_check) << 1);
let layout_flags =
i16::from(
self.tabs[i]
.scene
.document
.header
.paper_space_linetype_scaling,
) | (i16::from(self.tabs[i].scene.document.header.paper_space_limit_check) << 1);
for obj in self.tabs[i].scene.document.objects.values_mut() {
if let codec::objects::ObjectType::Layout(layout) = obj {
if layout.name == name {
@@ -141,20 +149,21 @@ impl OpenCADStudio {
/// Explicit fields win; everything else
/// keeps the layout's stored setup. Paper names resolve through the
/// catalog; scales are `"fit"` or `"paper:drawing"` like `"1:100"`.
pub(super) fn control_page_setup_set(
&mut self,
req: &Value,
) -> Result<Task<Message>, Value> {
pub(super) fn control_page_setup_set(&mut self, req: &Value) -> Result<Task<Message>, Value> {
let layout = string(req, "layout")?.to_owned();
let i = self.active_tab;
if !self.tabs[i].scene.layout_names().iter().any(|n| n.eq_ignore_ascii_case(&layout)) {
if !self.tabs[i]
.scene
.layout_names()
.iter()
.any(|n| n.eq_ignore_ascii_case(&layout))
{
return Err(failure(
"layout_missing",
format!("Layout '{layout}' does not exist"),
));
}
let mut ps = self
.tabs[i]
let mut ps = self.tabs[i]
.scene
.plot_settings_for(&layout)
.unwrap_or_else(|| codec::objects::PlotSettings::new(String::new()));
@@ -165,7 +174,10 @@ impl OpenCADStudio {
let (width, height) = resolved.portrait_mm();
ps.paper_width = width;
ps.paper_height = height;
ps.rotation = if req["orientation"].as_str().is_some_and(|o| o.eq_ignore_ascii_case("portrait")) {
ps.rotation = if req["orientation"]
.as_str()
.is_some_and(|o| o.eq_ignore_ascii_case("portrait"))
{
codec::objects::PlotRotation::None
} else {
codec::objects::PlotRotation::Degrees90
@@ -210,7 +222,10 @@ impl OpenCADStudio {
}
self.push_undo_snapshot(i, "PAGESETUP");
if !self.tabs[i].scene.set_layout_plot_settings(&layout, &ps) {
return Err(failure("layout_missing", format!("Layout '{layout}' vanished")));
return Err(failure(
"layout_missing",
format!("Layout '{layout}' vanished"),
));
}
self.tabs[i].dirty = true;
self.set_control_result(json!({
@@ -229,10 +244,7 @@ impl OpenCADStudio {
/// `entities_copy_to` — copy entities into another open document
/// Referenced layer definitions travel along;
/// the new handles are returned.
pub(super) fn control_entities_copy_to(
&mut self,
req: &Value,
) -> Result<Task<Message>, Value> {
pub(super) fn control_entities_copy_to(&mut self, req: &Value) -> Result<Task<Message>, Value> {
let handles = crate::app::control::entities::hex_handles(req, "handles")?;
let target = req["target_document_id"]
.as_u64()
@@ -334,13 +346,8 @@ impl OpenCADStudio {
return Err(failure("name_required", "Supply a set name"));
}
let handles = crate::app::control::entities::hex_handles(req, "handles")?;
let stored: Vec<String> = handles
.iter()
.map(|h| format!("{:X}", h.value()))
.collect();
self.control
.selection_sets
.insert(name.clone(), handles);
let stored: Vec<String> = handles.iter().map(|h| format!("{:X}", h.value())).collect();
self.control.selection_sets.insert(name.clone(), handles);
let count = self
.control
.selection_sets
@@ -364,10 +371,7 @@ impl OpenCADStudio {
.get(&name)
.cloned()
.ok_or_else(|| failure("selection_set_missing", format!("No set named '{name}'")))?;
let stored: Vec<String> = handles
.iter()
.map(|h| format!("{:X}", h.value()))
.collect();
let stored: Vec<String> = handles.iter().map(|h| format!("{:X}", h.value())).collect();
let select = req["select"].as_bool().unwrap_or(true);
let mut selected = 0;
if select {
@@ -541,8 +545,18 @@ pub(crate) fn new_guid_v4() -> String {
/// The readable registry; `WRITABLE_SYSVARS` is the subset `set` accepts.
const READABLE_SYSVARS: &[&str] = &[
"celtscale", "clayer", "ctextstyle", "extmax", "extmin", "filletrad",
"insunits", "ltscale", "mirrtext", "osmode", "pdmode", "pdsize",
"celtscale",
"clayer",
"ctextstyle",
"extmax",
"extmin",
"filletrad",
"insunits",
"ltscale",
"mirrtext",
"osmode",
"pdmode",
"pdsize",
"textsize",
];
@@ -653,22 +667,14 @@ fn set_sysvar(document: &mut codec::CadDocument, name: &str, value: &Value) -> R
Ok(())
}
fn positive(
value: &Value,
name: &str,
failure: impl Fn(&str) -> Value,
) -> Result<f64, Value> {
fn positive(value: &Value, name: &str, failure: impl Fn(&str) -> Value) -> Result<f64, Value> {
value
.as_f64()
.filter(|v| v.is_finite() && *v > 0.0)
.ok_or_else(|| failure(&format!("{name} needs a finite number > 0")))
}
fn integer(
value: &Value,
name: &str,
failure: impl Fn(&str) -> Value,
) -> Result<i16, Value> {
fn integer(value: &Value, name: &str, failure: impl Fn(&str) -> Value) -> Result<i16, Value> {
value
.as_i64()
.and_then(|v| i16::try_from(v).ok())
+42 -17
View File
@@ -195,17 +195,21 @@ pub(crate) mod windows_impl {
#[repr(C)]
struct ISpellCheckerFactoryVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
query_interface:
unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
get_supported_languages: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT,
get_supported_languages:
unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT,
is_supported: unsafe extern "system" fn(*mut c_void, *const u16, *mut BOOL) -> HRESULT,
create_spell_checker: unsafe extern "system" fn(*mut c_void, *const u16, *mut *mut c_void) -> HRESULT,
create_spell_checker:
unsafe extern "system" fn(*mut c_void, *const u16, *mut *mut c_void) -> HRESULT,
}
#[repr(C)]
struct ISpellCheckerVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
query_interface:
unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
get_language_tag: unsafe extern "system" fn(*mut c_void, *mut *mut u16) -> HRESULT,
@@ -222,7 +226,8 @@ pub(crate) mod windows_impl {
#[repr(C)]
struct IEnumSpellingErrorVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
query_interface:
unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
next: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT,
@@ -230,7 +235,8 @@ pub(crate) mod windows_impl {
#[repr(C)]
struct ISpellingErrorVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
query_interface:
unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
get_start_index: unsafe extern "system" fn(*mut c_void, *mut u32) -> HRESULT,
@@ -241,7 +247,8 @@ pub(crate) mod windows_impl {
#[repr(C)]
struct IEnumStringVtbl {
query_interface: unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
query_interface:
unsafe extern "system" fn(*mut c_void, *const GUID, *mut *mut c_void) -> HRESULT,
add_ref: unsafe extern "system" fn(*mut c_void) -> u32,
release: unsafe extern "system" fn(*mut c_void) -> u32,
next: unsafe extern "system" fn(*mut c_void, u32, *mut *mut u16, *mut u32) -> HRESULT,
@@ -266,7 +273,10 @@ pub(crate) mod windows_impl {
}
fn to_wide(s: &str) -> Vec<u16> {
std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect()
std::ffi::OsStr::new(s)
.encode_wide()
.chain(Some(0))
.collect()
}
unsafe fn from_wide_ptr(ptr: *mut u16) -> String {
@@ -367,8 +377,8 @@ pub(crate) mod windows_impl {
let has_err_exact = has_spelling_error(checker_vtbl, self.checker_obj, &wide_text);
let is_all_caps = word.chars().any(char::is_alphabetic)
&& word == word.to_uppercase();
let is_all_caps =
word.chars().any(char::is_alphabetic) && word == word.to_uppercase();
let mut is_misspelled = has_err_exact;
@@ -393,7 +403,11 @@ pub(crate) mod windows_impl {
wide_text.clone()
};
if ((*checker_vtbl).suggest)(self.checker_obj, target_wide.as_ptr(), &mut enum_str) == 0
if ((*checker_vtbl).suggest)(
self.checker_obj,
target_wide.as_ptr(),
&mut enum_str,
) == 0
&& !enum_str.is_null()
{
let enum_vtbl = *(enum_str as *const *const IEnumStringVtbl);
@@ -515,8 +529,8 @@ pub(crate) mod macos_impl {
}
unsafe {
let is_all_caps = word.chars().any(char::is_alphabetic)
&& word == word.to_uppercase();
let is_all_caps =
word.chars().any(char::is_alphabetic) && word == word.to_uppercase();
let ns_word = NSString::from_str(word);
let range: objc2_foundation::NSRange = msg_send![
@@ -551,7 +565,11 @@ pub(crate) mod macos_impl {
let mut suggestions = Vec::new();
if is_misspelled && want_suggestions {
let target_str = if is_all_caps { word.to_lowercase() } else { word.to_string() };
let target_str = if is_all_caps {
word.to_lowercase()
} else {
word.to_string()
};
let ns_target = NSString::from_str(&target_str);
let word_len = target_str.encode_utf16().count();
let r = objc2_foundation::NSRange {
@@ -679,7 +697,8 @@ pub(crate) mod linux_impl {
// If CLI hunspell is available, fallback to pipe check
if self.has_cli_tool {
if let Some((valid, suggs)) = check_via_cli(word, &self.language, want_suggestions) {
if let Some((valid, suggs)) = check_via_cli(word, &self.language, want_suggestions)
{
return (valid, suggs);
}
}
@@ -691,7 +710,9 @@ pub(crate) mod linux_impl {
}
fn load_wordlist(path: &Path, words: &mut HashSet<String>) -> bool {
let Ok(file) = File::open(path) else { return false };
let Ok(file) = File::open(path) else {
return false;
};
let reader = BufReader::new(file);
for line in reader.lines().flatten() {
let entry = line.trim();
@@ -718,7 +739,11 @@ pub(crate) mod linux_impl {
false
}
fn check_via_cli(word: &str, lang: &str, want_suggestions: bool) -> Option<(bool, Vec<String>)> {
fn check_via_cli(
word: &str,
lang: &str,
want_suggestions: bool,
) -> Option<(bool, Vec<String>)> {
use std::io::Write;
use std::process::{Command, Stdio};
+44 -26
View File
@@ -188,12 +188,7 @@ pub struct TextMatcher {
}
impl TextMatcher {
pub fn new(
search: String,
match_case: bool,
whole_word: bool,
ignore_accents: bool,
) -> Self {
pub fn new(search: String, match_case: bool, whole_word: bool, ignore_accents: bool) -> Self {
Self {
search,
match_case,
@@ -620,26 +615,42 @@ pub fn entity_text_info(
EntityType::Text(t) => {
let raw = t.value.clone();
let plain = decode_dxf_escapes(&raw);
let pos = [t.insertion_point.x, t.insertion_point.y, t.insertion_point.z];
let pos = [
t.insertion_point.x,
t.insertion_point.y,
t.insertion_point.z,
];
Some((raw, plain, pos))
}
EntityType::MText(t) => {
let raw = t.value.clone();
let plain_raw = codec::entities::mtext_format::parse_mtext(&raw, true).to_plain_text();
let plain = decode_dxf_escapes(&plain_raw);
let pos = [t.insertion_point.x, t.insertion_point.y, t.insertion_point.z];
let pos = [
t.insertion_point.x,
t.insertion_point.y,
t.insertion_point.z,
];
Some((raw, plain, pos))
}
EntityType::AttributeDefinition(ad) => {
let raw = ad.default_value.clone();
let plain = decode_dxf_escapes(&raw);
let pos = [ad.insertion_point.x, ad.insertion_point.y, ad.insertion_point.z];
let pos = [
ad.insertion_point.x,
ad.insertion_point.y,
ad.insertion_point.z,
];
Some((raw, plain, pos))
}
EntityType::AttributeEntity(ae) => {
let raw = ae.get_value().to_string();
let plain = decode_dxf_escapes(&raw);
let pos = [ae.insertion_point.x, ae.insertion_point.y, ae.insertion_point.z];
let pos = [
ae.insertion_point.x,
ae.insertion_point.y,
ae.insertion_point.z,
];
Some((raw, plain, pos))
}
EntityType::Dimension(dim) => {
@@ -1114,11 +1125,12 @@ impl OpenCADStudio {
continue;
}
let (is_system_valid, suggestions) = if let Some(ref speller) = system_speller {
speller.check_word(w, want_suggestions)
} else {
(false, Vec::new())
};
let (is_system_valid, suggestions) =
if let Some(ref speller) = system_speller {
speller.check_word(w, want_suggestions)
} else {
(false, Vec::new())
};
if !is_system_valid {
entity_had_match = true;
@@ -1149,8 +1161,7 @@ impl OpenCADStudio {
let plain = decode_dxf_escapes(raw);
for (idx, (matcher, replacement)) in pair_matchers.iter().enumerate() {
let (new_text, count) =
matcher.replace_all_in(&plain, replacement, true);
let (new_text, count) = matcher.replace_all_in(&plain, replacement, true);
if count > 0 {
pair_hit_counts[idx] += count;
total_occurrences += count;
@@ -1199,11 +1210,12 @@ impl OpenCADStudio {
continue;
}
let (is_system_valid, suggestions) = if let Some(ref speller) = system_speller {
speller.check_word(w, want_suggestions)
} else {
(false, Vec::new())
};
let (is_system_valid, suggestions) =
if let Some(ref speller) = system_speller {
speller.check_word(w, want_suggestions)
} else {
(false, Vec::new())
};
if !is_system_valid {
entity_had_match = true;
@@ -1339,7 +1351,12 @@ impl OpenCADStudio {
let matchers: Vec<(TextMatcher, String)> = pairs
.into_iter()
.map(|(f, r)| (TextMatcher::new(f, match_case, whole_word, ignore_accents), r))
.map(|(f, r)| {
(
TextMatcher::new(f, match_case, whole_word, ignore_accents),
r,
)
})
.collect();
if push_undo && !dry_run {
@@ -1694,14 +1711,16 @@ mod tests {
// Test 1: Plain LF query matching raw \P
let raw1 = "Loge \\Pgardien";
let matcher1 = TextMatcher::new("Loge \ngardien".into(), false, false, true);
let (replaced1, count1) = replace_mtext_safe(raw1, &matcher1, "Security \nGuard Booth", true);
let (replaced1, count1) =
replace_mtext_safe(raw1, &matcher1, "Security \nGuard Booth", true);
assert_eq!(count1, 1);
assert_eq!(replaced1, "Security \\PGuard Booth");
// Test 2: Multiline with complex formatting codes preserved
let raw2 = "\\pxqc;{\\fCentury Gothic|b1|i0|c0|p34;ACCÈS ET SORTIE\\PMARCHANDISES}";
let matcher2 = TextMatcher::new("ACCÈS ET SORTIE\nMARCHANDISES".into(), false, false, true);
let (replaced2, count2) = replace_mtext_safe(raw2, &matcher2, "GOODS ACCESS AND EXIT", true);
let (replaced2, count2) =
replace_mtext_safe(raw2, &matcher2, "GOODS ACCESS AND EXIT", true);
assert_eq!(count2, 1);
assert!(replaced2.contains("GOODS ACCESS AND EXIT"));
assert!(replaced2.contains("\\pxqc;"));
@@ -1715,4 +1734,3 @@ mod tests {
assert_eq!(replaced3, "{\\pqc;WAREHOUSE OFFICE}");
}
}
+39 -15
View File
@@ -4,16 +4,36 @@ use serde_json::{json, Value};
/// 5x7 bitmapped font glyphs for digits '0' through '9'
const DIGITS_5X7: [[u8; 7]; 10] = [
[0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110], // 0
[0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], // 1
[0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111], // 2
[0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110], // 3
[0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010], // 4
[0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110], // 5
[0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110], // 6
[0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000], // 7
[0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110], // 8
[0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100], // 9
[
0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110,
], // 0
[
0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110,
], // 1
[
0b01110, 0b10001, 0b00001, 0b00110, 0b01000, 0b10000, 0b11111,
], // 2
[
0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110,
], // 3
[
0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010,
], // 4
[
0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110,
], // 5
[
0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110,
], // 6
[
0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000,
], // 7
[
0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110,
], // 8
[
0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100,
], // 9
];
/// Draw a Set-of-Marks badge at `(center_x, center_y)` onto `image`.
@@ -220,7 +240,8 @@ pub(crate) fn compute_grounding(
for entity in tab.scene.document.entities() {
let handle = entity.common().handle;
let (b_min, b_max) = crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, entity);
let (b_min, b_max) =
crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, entity);
let min_dvec = glam::DVec3::from(b_min);
let max_dvec = glam::DVec3::from(b_max);
if !min_dvec.is_finite() || !max_dvec.is_finite() {
@@ -277,9 +298,11 @@ pub(crate) fn compute_grounding(
// Selected entities prioritized, then larger visual area first
candidates.sort_by(|a, b| {
b.selected
.cmp(&a.selected)
.then_with(|| b.area.partial_cmp(&a.area).unwrap_or(std::cmp::Ordering::Equal))
b.selected.cmp(&a.selected).then_with(|| {
b.area
.partial_cmp(&a.area)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
candidates.truncate(64);
@@ -373,7 +396,8 @@ pub(crate) fn compute_visual_diff(
diff_overlay.put_pixel(x, y, image::Rgba([0, 255, 128, 255]));
} else {
// Dim unchanged background: grayscale attenuated to 35%
let gray = ((p2[0] as f32 * 0.299 + p2[1] as f32 * 0.587 + p2[2] as f32 * 0.114) * 0.35) as u8;
let gray = ((p2[0] as f32 * 0.299 + p2[1] as f32 * 0.587 + p2[2] as f32 * 0.114)
* 0.35) as u8;
diff_overlay.put_pixel(x, y, image::Rgba([gray, gray, gray, 200]));
}
}