forked from 1b-agent/opencadstudio-upstream
style: apply cargo fmt across workspace
This commit is contained in:
1 parent
85710e5454
commit
9cf9ff2c5c
531 files changed
+57740
-33906
No files matched your search
+296
-97
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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]));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user