mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Squash of PR #533 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0230250bf5
commit
bc7744b27b
10 files changed
+395
-4
No files matched your search
@@ -27,6 +27,11 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
.alias(&["xa"])
|
||||
.params("{path: DWG/DXF file (saved as given; a relative path is relative to this drawing's folder), name? (default: the file name), overlay?: bool, at?: [x,y] (default 0,0), scale?: number | [x, y], rotation? (degrees)} → {name, handle, file}")
|
||||
.interactive(|_| Ok(Box::new(XrefM::new("XATTACH", XStage::File { overlay: false })))),
|
||||
CommandSpec::new("xref", "Reference Manager", run_xref)
|
||||
.menu(&["Window", "Reference Manager"])
|
||||
.alias(&["xr", "externalreferences"])
|
||||
.params("{} → {xrefs: [{name, path, foundAt, type: Attach|Overlay, status: Loaded|Unloaded|Not Found|Unresolved|Unreferenced|Not loaded, references, objects}]} (the app opens the External References palette instead)")
|
||||
.noundo(),
|
||||
CommandSpec::new("-xref", "External References", run_dash_xref)
|
||||
.alias(&["-xr"])
|
||||
.params("{option: \"?\"|bind|detach|path|unload|reload|overlay|attach|type, names?: \"A,B*\" (default *), path? (path, attach, overlay), insert?: bool (bind: Insert type), overlay?: bool (type), at?, scale?, rotation? (attach)}")
|
||||
@@ -199,6 +204,16 @@ fn named(s: &Session, cmd: &str, p: &Value) -> Result<Vec<String>> {
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
/// XREF's JSON form: the xrefs as the External References palette lists them (the palette is
|
||||
/// the app's; typed without a UI, the list is echoed).
|
||||
fn run_xref(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let r = run_dash_xref(s, &json!({ "option": "?" }))?;
|
||||
if r["xrefs"].as_array().is_some_and(|a| a.is_empty()) {
|
||||
s.echo("No xrefs are attached.");
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn run_dash_xref(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let option = str_param(p, "option").unwrap_or("?").trim().to_ascii_lowercase();
|
||||
let host = host(s);
|
||||
|
||||
@@ -13,16 +13,19 @@ pub enum Panel {
|
||||
ToolSets,
|
||||
Layers,
|
||||
Properties,
|
||||
#[serde(rename = "externalReferences")]
|
||||
ExternalRefs,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
pub const ALL: &[Self] = &[Self::Canvas, Self::ToolSets, Self::Layers, Self::Properties];
|
||||
pub const ALL: &[Self] = &[Self::Canvas, Self::ToolSets, Self::Layers, Self::Properties, Self::ExternalRefs];
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Canvas => "canvas",
|
||||
Self::ToolSets => "toolSets",
|
||||
Self::Layers => "layers",
|
||||
Self::Properties => "properties",
|
||||
Self::ExternalRefs => "externalReferences",
|
||||
}
|
||||
}
|
||||
fn label(self) -> &'static str {
|
||||
@@ -31,6 +34,7 @@ impl Panel {
|
||||
Self::ToolSets => "Tool Sets",
|
||||
Self::Layers => "Layers",
|
||||
Self::Properties => "Properties",
|
||||
Self::ExternalRefs => "External References",
|
||||
}
|
||||
}
|
||||
fn default_side(self) -> Zone {
|
||||
@@ -39,6 +43,7 @@ impl Panel {
|
||||
Self::ToolSets => Zone::Left,
|
||||
Self::Layers => Zone::Right,
|
||||
Self::Properties => Zone::Right,
|
||||
Self::ExternalRefs => Zone::Right,
|
||||
}
|
||||
}
|
||||
fn parse(id: &str) -> Result<Self, String> {
|
||||
@@ -349,6 +354,9 @@ pub fn show(app: &mut crate::CadApp, ui: &mut egui::Ui) {
|
||||
crate::shortcut_menus::palette(app, ui, crate::palettes::properties_section, crate::shortcut_menus::properties_entries)
|
||||
});
|
||||
}
|
||||
Panel::ExternalRefs => {
|
||||
egui::ScrollArea::both().id_salt("dock-xrefs").auto_shrink([false, false]).show(ui, |ui| crate::xrefs::section(app, ui));
|
||||
}
|
||||
},
|
||||
);
|
||||
// Existing panel close buttons and commands still own their visibility switches.
|
||||
@@ -377,7 +385,7 @@ fn panel_limits(panel: &Panel) -> PanelLimits {
|
||||
let (min, max_width) = match panel {
|
||||
Panel::Canvas => (egui::vec2(300.0, 180.0), 1_000_000.0),
|
||||
Panel::ToolSets => (egui::vec2(220.0, 150.0), 480.0),
|
||||
Panel::Layers | Panel::Properties => (egui::vec2(220.0, 100.0), 720.0),
|
||||
Panel::Layers | Panel::Properties | Panel::ExternalRefs => (egui::vec2(220.0, 100.0), 720.0),
|
||||
};
|
||||
PanelLimits { min, max: egui::vec2(max_width, 1_000_000.0) }
|
||||
}
|
||||
@@ -63,6 +63,7 @@ fn ukrainian_covers_commands_menus_tools_and_snap_labels() {
|
||||
fn every_marked_ui_literal_has_a_translation() {
|
||||
let catalog = catalog("uk").unwrap();
|
||||
let sources = [
|
||||
include_str!("../xrefs.rs"),
|
||||
include_str!("../canvas.rs"),
|
||||
include_str!("../chrome.rs"),
|
||||
include_str!("../cmdline.rs"),
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
# UTF-8: context<TAB>English source<TAB>Ukrainian translation.
|
||||
# Empty context = ordinary label; @plural = one|few|many forms with {n}.
|
||||
# Keep command IDs, keywords, technology/product names and user data unchanged.
|
||||
# External References palette (#525).
|
||||
Apply Застосувати
|
||||
Attach DWG... Приєднати DWG...
|
||||
Bind Вбудувати
|
||||
Detach Від'єднати
|
||||
Loaded Завантажено
|
||||
Make it a block of this drawing, keeping its layer names Зробити блоком цього креслення зі збереженням назв шарів
|
||||
Make it a block of this drawing; its layers become NAME$0$LAYER Зробити блоком цього креслення; його шари стануть NAME$0$LAYER
|
||||
No external references are attached. Зовнішні посилання не приєднано.
|
||||
Not Found Не знайдено
|
||||
Not loaded Не завантажено
|
||||
Open Відкрити
|
||||
Overlay Накладення
|
||||
Reference Manager Диспетчер посилань
|
||||
Reference Name Ім'я посилання
|
||||
References: {n} Посилань: {n}
|
||||
Reload Оновити
|
||||
Reload All Оновити всі
|
||||
Saved Path Збережений шлях
|
||||
Unload Вивантажити
|
||||
Unloaded Вивантажено
|
||||
Unreferenced Без посилань
|
||||
Unresolved Не розв'язано
|
||||
Cancel Скасувати
|
||||
OK Гаразд
|
||||
or [ або [
|
||||
|
||||
|
@@ -19,6 +19,8 @@ pub mod control;
|
||||
pub mod credits;
|
||||
pub mod dialogs;
|
||||
pub mod docking;
|
||||
// The External References palette.
|
||||
pub mod xrefs;
|
||||
|
||||
pub mod dyninput;
|
||||
pub mod gpu;
|
||||
|
||||
@@ -69,6 +69,9 @@ pub fn run_ui_command(app: &mut CadApp, id: &str, params: &Value) -> Option<Resu
|
||||
}
|
||||
|
||||
fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
|
||||
if let Some(r) = crate::xrefs::route(app, id, params) {
|
||||
return Some(r);
|
||||
}
|
||||
if let Some(r) = crate::managers::route(app, id, params) {
|
||||
return Some(r);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
//! The External References palette: the drawing's xrefs with their status, type and path, and
|
||||
//! Attach, Reload, Unload, Detach, Bind, Path and Open. A thin view over the engine's XREF
|
||||
//! commands (`xref`, `-xref`, `xattach`, `xopen`): it reads the drawing and acts through
|
||||
//! [`CadApp::run`] / [`CadApp::start`].
|
||||
|
||||
use egui::RichText;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::CadApp;
|
||||
use crate::docking::Panel;
|
||||
|
||||
/// XREF typed or chosen from a menu opens this palette; XATTACH there asks for the file first
|
||||
/// (with the host's file picker, where there is one). JSON calls (with parameters) run the
|
||||
/// engine's commands as they are.
|
||||
pub fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {
|
||||
if !params.is_null() {
|
||||
return None;
|
||||
}
|
||||
match cadcraft_engine::cmd::resolve_alias(id).as_str() {
|
||||
"xref" => Some(open_palette(app)),
|
||||
"xattach" => {
|
||||
let picked = app.services.pick_open.as_ref().and_then(|f| f())?;
|
||||
let path = relative_to_host(app, &picked);
|
||||
if let Err(e) = app.session.start("xattach") {
|
||||
return Some(Err(e.to_string()));
|
||||
}
|
||||
app.cmdline(&path);
|
||||
Some(Ok(Value::Null))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the palette. The first time it docks below the Layers palette (where the palettes column
|
||||
/// is); after that it comes back where it was.
|
||||
fn open_palette(app: &mut CadApp) -> Result<Value, String> {
|
||||
let panel = Panel::ExternalRefs;
|
||||
let first = !app.ui.docking.layout.contains(&panel) && !app.ui.docking.hidden.iter().any(|(p, _)| *p == panel);
|
||||
let r = crate::docking::command(app, false, &json!({ "operation": "open", "panel": panel.id() }))?;
|
||||
if first && app.ui.docking.layout.contains(&Panel::Layers) {
|
||||
let moved = json!({ "operation": "move", "panel": panel.id(), "target": Panel::Layers.id(), "zone": "bottom" });
|
||||
if let Ok(r) = crate::docking::command(app, false, &moved) {
|
||||
return Ok(r);
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// A picked file inside the host drawing's folder is attached by its relative path, so the
|
||||
/// project folder can move.
|
||||
fn relative_to_host(app: &CadApp, picked: &str) -> String {
|
||||
let host = app.session.state().ok().and_then(|st| st.path.clone());
|
||||
let dir = host.as_deref().and_then(|h| std::path::Path::new(h).parent());
|
||||
match dir.and_then(|d| std::path::Path::new(picked).strip_prefix(d).ok()) {
|
||||
Some(rel) if !rel.as_os_str().is_empty() => rel.to_string_lossy().into_owned(),
|
||||
_ => picked.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// One xref as the palette lists it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Row {
|
||||
name: String,
|
||||
status: String,
|
||||
overlay: bool,
|
||||
path: String,
|
||||
references: usize,
|
||||
}
|
||||
|
||||
/// The xrefs of the active drawing (cached per drawing revision: counting references walks it).
|
||||
fn rows(app: &CadApp, ui: &egui::Ui) -> Vec<Row> {
|
||||
let Ok(st) = app.session.state() else { return Vec::new() };
|
||||
let key = (st.uid, st.revision);
|
||||
let id = egui::Id::new("xref-rows");
|
||||
if let Some((k, rows)) = ui.data(|d| d.get_temp::<((u64, u64), Vec<Row>)>(id))
|
||||
&& k == key
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
let d = &st.doc;
|
||||
let xrefs: Vec<_> = d.blocks.values().filter(|b| b.xref_path.is_some() && !b.name.contains('|')).collect();
|
||||
let mut counts = vec![0usize; xrefs.len()];
|
||||
if !xrefs.is_empty() {
|
||||
let mut count = |store: &cadcraft_engine::doc::EntityStore| {
|
||||
for e in store.iter() {
|
||||
if let cadcraft_engine::doc::EntityKind::Insert(i) = &e.kind
|
||||
&& let Some(k) = xrefs.iter().position(|b| b.name.eq_ignore_ascii_case(&i.block))
|
||||
&& let Some(c) = counts.get_mut(k)
|
||||
{
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
count(&d.model);
|
||||
d.layouts.iter().for_each(|l| count(&l.entities));
|
||||
d.blocks.values().filter(|b| !b.name.contains('|')).for_each(|b| count(&b.entities));
|
||||
}
|
||||
let rows: Vec<Row> = xrefs
|
||||
.iter()
|
||||
.zip(counts)
|
||||
.map(|(b, references)| {
|
||||
let loaded = b.xref_status == cadcraft_engine::doc::XrefStatus::Loaded;
|
||||
Row {
|
||||
name: b.name.clone(),
|
||||
status: if loaded && references == 0 { "Unreferenced".into() } else { b.xref_status.label().into() },
|
||||
overlay: b.xref_overlay,
|
||||
path: b.xref_path.clone().unwrap_or_default(),
|
||||
references,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
ui.data_mut(|d| d.insert_temp(id, (key, rows.clone())));
|
||||
rows
|
||||
}
|
||||
|
||||
/// What a click in the palette asks for, run once the palette is drawn.
|
||||
enum Act {
|
||||
Start(&'static str),
|
||||
Run(&'static str, Value),
|
||||
}
|
||||
|
||||
/// The palette body.
|
||||
pub fn section(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
let t = crate::theme::Tokens::get();
|
||||
let rows = rows(app, ui);
|
||||
let sel_id = egui::Id::new("xref-selected");
|
||||
let path_id = egui::Id::new("xref-path-edit");
|
||||
let mut selected: Option<String> = ui.data(|d| d.get_temp(sel_id));
|
||||
if !selected.as_ref().is_some_and(|s| rows.iter().any(|r| &r.name == s)) {
|
||||
selected = rows.first().map(|r| r.name.clone());
|
||||
}
|
||||
let mut act = None;
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
if ui.button(crate::tl!("Attach DWG...")).clicked() {
|
||||
act = Some(Act::Start("xattach"));
|
||||
}
|
||||
if ui.add_enabled(!rows.is_empty(), egui::Button::new(crate::tl!("Reload All"))).clicked() {
|
||||
act = Some(Act::Run("-xref", json!({ "option": "reload", "names": "*" })));
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
if rows.is_empty() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(crate::tl!("No external references are attached.")).color(t.text_dim));
|
||||
});
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("xref-grid").num_columns(4).striped(true).spacing([10.0, 4.0]).show(ui, |ui| {
|
||||
for h in [crate::tl!("Reference Name"), crate::tl!("Status"), crate::tl!("Type"), crate::tl!("Saved Path")] {
|
||||
ui.label(RichText::new(h).color(t.text_dim));
|
||||
}
|
||||
ui.end_row();
|
||||
for r in &rows {
|
||||
let on = selected.as_deref() == Some(r.name.as_str());
|
||||
if ui.selectable_label(on, &r.name).clicked() {
|
||||
selected = Some(r.name.clone());
|
||||
ui.data_mut(|d| d.remove::<String>(path_id));
|
||||
}
|
||||
let bad = matches!(r.status.as_str(), "Not Found" | "Unresolved");
|
||||
ui.label(RichText::new(crate::i18n::t(&r.status)).color(if bad { t.warn } else { t.text }));
|
||||
ui.label(crate::i18n::t(if r.overlay { "Overlay" } else { "Attach" }));
|
||||
ui.label(&r.path).on_hover_text(crate::tf!("References: {n}", n = r.references));
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.data_mut(|d| match &selected {
|
||||
Some(s) => {
|
||||
d.insert_temp(sel_id, s.clone());
|
||||
}
|
||||
None => d.remove::<String>(sel_id),
|
||||
});
|
||||
if let Some(r) = selected.as_ref().and_then(|s| rows.iter().find(|r| &r.name == s)) {
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
details(ui, r, path_id, &mut act);
|
||||
}
|
||||
match act {
|
||||
Some(Act::Start(id)) => app.start(id),
|
||||
Some(Act::Run(id, p)) => {
|
||||
if let Err(e) = app.run(id, p) {
|
||||
app.set_status(e);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected xref: its actions, type and path.
|
||||
fn details(ui: &mut egui::Ui, r: &Row, path_id: egui::Id, act: &mut Option<Act>) {
|
||||
let name = r.name.clone();
|
||||
let loaded = r.status == "Loaded" || r.status == "Unreferenced";
|
||||
let op = |option: &str| json!({ "option": option, "names": name });
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.add_space(8.0);
|
||||
if ui.button(crate::tl!("Reload")).clicked() {
|
||||
*act = Some(Act::Run("-xref", op("reload")));
|
||||
}
|
||||
if ui.add_enabled(r.status != "Unloaded", egui::Button::new(crate::tl!("Unload"))).clicked() {
|
||||
*act = Some(Act::Run("-xref", op("unload")));
|
||||
}
|
||||
if ui.button(crate::tl!("Detach")).clicked() {
|
||||
*act = Some(Act::Run("-xref", op("detach")));
|
||||
}
|
||||
if ui
|
||||
.add_enabled(loaded, egui::Button::new(crate::tl!("Bind")))
|
||||
.on_hover_text(crate::tl!("Make it a block of this drawing; its layers become NAME$0$LAYER"))
|
||||
.clicked()
|
||||
{
|
||||
*act = Some(Act::Run("-xref", op("bind")));
|
||||
}
|
||||
if ui
|
||||
.add_enabled(loaded, egui::Button::new(crate::tl!("Insert")))
|
||||
.on_hover_text(crate::tl!("Make it a block of this drawing, keeping its layer names"))
|
||||
.clicked()
|
||||
{
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "bind", "names": name, "insert": true })));
|
||||
}
|
||||
if ui.add_enabled(r.status != "Not Found", egui::Button::new(crate::tl!("Open"))).clicked() {
|
||||
*act = Some(Act::Run("xopen", json!({ "name": name })));
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(crate::tl!("Type"));
|
||||
let mut overlay = r.overlay;
|
||||
let a = ui.radio_value(&mut overlay, false, crate::tl!("Attach")).clicked();
|
||||
let o = ui.radio_value(&mut overlay, true, crate::tl!("Overlay")).clicked();
|
||||
if (a || o) && overlay != r.overlay {
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "type", "names": name, "overlay": overlay })));
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.label(crate::tl!("Saved Path"));
|
||||
let mut text: String = ui.data(|d| d.get_temp(path_id)).unwrap_or_else(|| r.path.clone());
|
||||
let w = (ui.available_width() - 70.0).max(60.0);
|
||||
let edit = ui.add(egui::TextEdit::singleline(&mut text).desired_width(w));
|
||||
let apply = ui.add_enabled(!text.trim().is_empty() && text != r.path, egui::Button::new(crate::tl!("Apply"))).clicked()
|
||||
|| (edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && text != r.path);
|
||||
if apply {
|
||||
*act = Some(Act::Run("-xref", json!({ "option": "path", "names": name, "path": text.trim() })));
|
||||
ui.data_mut(|d| d.remove::<String>(path_id));
|
||||
} else if text != r.path {
|
||||
ui.data_mut(|d| d.insert_temp(path_id, text));
|
||||
} else {
|
||||
ui.data_mut(|d| d.remove::<String>(path_id));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//! The External References palette (#525): XREF opens it; its rows and buttons act through the
|
||||
//! engine's -XREF commands.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::cmd::file::{IoHooks, set_io};
|
||||
use cadcraft_engine::doc::geom::Vec3;
|
||||
use cadcraft_engine::doc::{Drawing, Entity, EntityKind, Handle, Layer, Line, XrefStatus};
|
||||
use cadcraft_ui_egui::{CadApp, Services, docking::Panel};
|
||||
use egui::vec2;
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
/// Test drawings: any file holds one line on layer Walls.
|
||||
fn read(_bytes: &[u8], _name: &str) -> Result<Drawing, String> {
|
||||
let mut d = Drawing::new_metric();
|
||||
d.layers.push(Layer { name: "Walls".into(), ..Layer::default() });
|
||||
let mut e = Entity::new(Handle(0x60), EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(50.0, 20.0, 0.0) }));
|
||||
e.common.layer = "Walls".into();
|
||||
d.model.push(e);
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
fn site_file() -> String {
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-xref-palette-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let p = dir.join("site.dxf");
|
||||
std::fs::write(&p, "site").unwrap();
|
||||
p.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn status(app: &CadApp) -> Option<XrefStatus> {
|
||||
app.session.doc().ok()?.block("site").map(|b| b.xref_status)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_opens_the_palette_and_its_buttons_manage_the_xref() {
|
||||
set_io(IoHooks { read, write: |_, _| Err("no writer in this test".into()), plot: None });
|
||||
let mut app = CadApp::new(Session::default(), Services::default());
|
||||
app.run("xattach", json!({ "path": site_file(), "at": [10, 10] })).unwrap();
|
||||
assert_eq!(status(&app), Some(XrefStatus::Loaded));
|
||||
// Typed (by its alias) it opens the palette instead of listing.
|
||||
assert!(app.cmdline("XR").is_none());
|
||||
assert!(app.ui.docking.layout.contains(&Panel::ExternalRefs));
|
||||
app.ui.in_window_menu = false;
|
||||
let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut CadApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(4);
|
||||
h.get_by_label("site");
|
||||
h.get_by_label("Loaded");
|
||||
h.get_by_label("Unload").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(status(h.state()), Some(XrefStatus::Unloaded));
|
||||
h.get_by_label("Unloaded");
|
||||
h.get_by_label("Reload").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(status(h.state()), Some(XrefStatus::Loaded));
|
||||
h.get_by_label("Overlay").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().session.doc().unwrap().block("site").unwrap().xref_overlay);
|
||||
h.get_by_label("Detach").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(status(h.state()), None);
|
||||
h.get_by_label("No external references are attached.");
|
||||
// Undo brings it back, and the palette lists it again.
|
||||
h.state_mut().session.undo().unwrap();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("site");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_xref_lists_and_never_opens_the_palette() {
|
||||
let mut app = CadApp::new(Session::default(), Services::default());
|
||||
let r = app.run("xref", json!({})).unwrap();
|
||||
assert_eq!(r["xrefs"], json!([]));
|
||||
assert!(!app.ui.docking.layout.contains(&Panel::ExternalRefs));
|
||||
}
|
||||
@@ -65,7 +65,7 @@ file before restarting to resume saving workspace choices.
|
||||
|
||||
Tool Sets, Layers, and Properties can be regrouped, split, floated within the app window, closed, and reopened. The drawing canvas stays visible and cannot be closed or turned into a tab. The document/session view is protected. Layouts, active tabs, panel visibility, and floating rectangles are saved with the app's existing UI preferences. These operations do not edit document contents.
|
||||
|
||||
Panel IDs: `toolSets`, `layers`, `properties`. The protected center is `canvas`.
|
||||
Panel IDs: `toolSets`, `layers`, `properties`, `externalReferences`. The protected center is `canvas`.
|
||||
|
||||
`ui.dock` accepts `operation`; basic per-panel operations also require `panel`. The advanced `resizeSplit` operation below uses a split `path` instead of a panel. Basic operations are `open`, `close`, `activate`, `float` (with finite positive `rect: [x, y, width, height]`), or `move` (with `target` and `zone: center|left|right|top|bottom`). Center groups with the target; side zones split beside it. `ui.dock.reset` restores the default arrangement. Invalid moves return an error without changing the layout.
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ do them first.
|
||||
| # | Gap | Evidence | User impact | Est. |
|
||||
|---|---|---|---|---|
|
||||
| F1 | **[alpha blocker]** Prompt options print "not available yet": PEDIT Edit vertex Break/Regen/Tangent, some SPLINEDIT options; MTEXT Dynamic/Static columns (no column model). SPLINE, MTEXT and MLEADER options done (#408): closed splines keep but ignore a fit tolerance, SPLINE Object fits the polyline's vertices (no spline frame is kept) (TRIM/EXTEND, PLINE arc mode and Length, XLINE done (#397); OFFSET, ROTATE and FILLET options done earlier) | `modify2.rs:594,783,806`, `cmd/mtext_opts.rs` | Medium: the most-used commands are done | 3–6 |
|
||||
| F2 | **[alpha blocker]** No xrefs (XATTACH, XREF palette, REFEDIT, BIND, XCLIP, overlay, paths); DXF xref blocks read with an empty path | `dxf_read.rs:923` | High: multi-file projects | 30–45 |
|
||||
| F2 | XREF editing in place and clipping: REFEDIT and XCLIP are missing. XATTACH, the External References palette, -XREF (Bind/Detach/Path/Unload/Reload/Overlay), XBIND, XOPEN, overlays, relative paths and loading at open are done (#525). Unloaded state and xref-layer overrides last only for the session | `cmd/blocks/xref.rs` | Medium: xrefs can't be edited in place or clipped | 15–25 |
|
||||
| F3 | **[alpha blocker]** (block editor and REFEDIT; dynamic-block authoring is beta) No block editor (BEDIT) or dynamic blocks (parameters, actions, visibility, lookup); dynamic blocks from other files lose their behaviour | issue #185 offers help | High: standard libraries are dynamic | 50–75 |
|
||||
| F4 | Autosave leftovers: no autosave in the web build (browser storage); autosave files are DXF, not DWG; no AUDITCTL `.adt` log and no separate RECOVER mode in the file readers (SAVETIME, Drawing Recovery, AUDIT and RECOVER landed with #406) | `engine/src/autosave.rs`, `engine/src/audit.rs` | Low-medium (web users) | 3–5 |
|
||||
| F5 | Arrays are not associative: no ARRAYEDIT; ARRAY/ARRAYRECT/ARRAYPOLAR edit the array in an option loop, then create plain copies (ASsociative, Levels, polar ROWs and grip editing print "not available yet") | `cmd/array.rs` | Medium-high | 8–12 |
|
||||
|
||||
Reference in new issue
Block a user