forked from 1b-agent/opencadstudio-upstream
Merge pull request #1752 from ramox81/codex/refedit-parity
REFEDIT: reference prompts, -REFEDIT, REFSET, nested references and xref write-back
This commit is contained in:
commit
7e763080cb
16 files changed
+1296
-380
No files matched your search
@@ -6456,3 +6456,35 @@ view-background =
|
||||
.help = Sets the background a named view shows when it is restored.
|
||||
.image-files = Image files
|
||||
.select-image = Select Background Image
|
||||
|
||||
ref-edit =
|
||||
.select-reference = Select reference:
|
||||
.reference-not-found = Reference not found.
|
||||
.not-valid-reference = Object is not a valid reference
|
||||
.one-entity-added = 1 entity added
|
||||
.nesting-level = Select nesting level [Ok/Next] <Next>:
|
||||
.selection-method = Enter object selection method [All/Nested] <All>:
|
||||
.select-nested = Select nested objects:
|
||||
.display-attdefs = Display attribute definitions [Yes/No] <No>:
|
||||
.zero-items-selected = 0 items selected
|
||||
.one-item-selected = 1 item selected
|
||||
.items-selected = __ocs_fmt_0__ items selected
|
||||
.refset-option = Enter an option [Add/Remove] <Add>:
|
||||
.refclose-option = Enter option [Save/Discard reference changes] <Save>:
|
||||
.not-checked-out = ** Command not allowed unless a reference is checked out with REFEDIT command **
|
||||
.already-checked-out = ** Command not allowed, __ocs_fmt_0__ already checked out for editing **
|
||||
.transfer-objects = Transfer objects between the RefEdit working set and host drawing...
|
||||
.added-to-working-set = __ocs_fmt_0__ Added to working set
|
||||
.removed-from-working-set = __ocs_fmt_0__ Removed from working set
|
||||
.one-object-added = 1 object added to __ocs_fmt_0__
|
||||
.objects-added = __ocs_fmt_0__ objects added to __ocs_fmt_1__
|
||||
.one-object-removed = 1 object removed from __ocs_fmt_0__
|
||||
.objects-removed = __ocs_fmt_0__ objects removed from __ocs_fmt_1__
|
||||
.one-xref-instance = 1 xref instance updated
|
||||
.xref-instances = __ocs_fmt_0__ xref instances updated
|
||||
.one-block-instance = 1 block instance updated
|
||||
.block-instances = __ocs_fmt_0__ block instances updated
|
||||
.redefined-reloaded = __ocs_fmt_0__ redefined and reloaded.
|
||||
.redefined = __ocs_fmt_0__ redefined.
|
||||
.ok = Ok
|
||||
.nested = Nested
|
||||
@@ -6378,3 +6378,35 @@ view-background =
|
||||
.help = Adlandırılmış görünüm geri yüklendiğinde gösterilecek arka planı ayarlar.
|
||||
.image-files = Görüntü dosyaları
|
||||
.select-image = Arka Plan Görüntüsü Seç
|
||||
|
||||
ref-edit =
|
||||
.select-reference = Referans seçin:
|
||||
.reference-not-found = Referans bulunamadı.
|
||||
.not-valid-reference = Nesne geçerli bir referans değil
|
||||
.one-entity-added = 1 nesne eklendi
|
||||
.nesting-level = İç içe düzeyi seçin [Tamam/Sonraki] <Sonraki>:
|
||||
.selection-method = Nesne seçim yöntemini girin [Tümü/İç içe] <Tümü>:
|
||||
.select-nested = İç içe nesneleri seçin:
|
||||
.display-attdefs = Öznitelik tanımları gösterilsin mi [Evet/Hayır] <Hayır>:
|
||||
.zero-items-selected = 0 öğe seçildi
|
||||
.one-item-selected = 1 öğe seçildi
|
||||
.items-selected = __ocs_fmt_0__ öğe seçildi
|
||||
.refset-option = Bir seçenek girin [Ekle/Kaldır] <Ekle>:
|
||||
.refclose-option = Seçeneği girin [Kaydet/Referans değişikliklerinden vazgeç] <Kaydet>:
|
||||
.not-checked-out = ** Bir referans REFEDIT komutuyla düzenlemeye alınmadıkça komuta izin verilmez **
|
||||
.already-checked-out = ** Komuta izin verilmez, __ocs_fmt_0__ zaten düzenlemeye alınmış **
|
||||
.transfer-objects = Nesneleri RefEdit çalışma kümesi ile ana çizim arasında aktarın...
|
||||
.added-to-working-set = __ocs_fmt_0__ nesne çalışma kümesine eklendi
|
||||
.removed-from-working-set = __ocs_fmt_0__ nesne çalışma kümesinden çıkarıldı
|
||||
.one-object-added = 1 nesne __ocs_fmt_0__ tanımına eklendi
|
||||
.objects-added = __ocs_fmt_0__ nesne __ocs_fmt_1__ tanımına eklendi
|
||||
.one-object-removed = 1 nesne __ocs_fmt_0__ tanımından çıkarıldı
|
||||
.objects-removed = __ocs_fmt_0__ nesne __ocs_fmt_1__ tanımından çıkarıldı
|
||||
.one-xref-instance = 1 xref örneği güncellendi
|
||||
.xref-instances = __ocs_fmt_0__ xref örneği güncellendi
|
||||
.one-block-instance = 1 blok örneği güncellendi
|
||||
.block-instances = __ocs_fmt_0__ blok örneği güncellendi
|
||||
.redefined-reloaded = __ocs_fmt_0__ yeniden tanımlandı ve yeniden yüklendi.
|
||||
.redefined = __ocs_fmt_0__ yeniden tanımlandı.
|
||||
.ok = Tamam
|
||||
.nested = İç içe
|
||||
@@ -80,7 +80,7 @@ use crate::command::{CmdResult, SelectionEntity, StepInput};
|
||||
use codec::Handle;
|
||||
use iced::Task;
|
||||
|
||||
mod clipboard;
|
||||
pub(super) mod clipboard;
|
||||
mod constraint;
|
||||
mod dim_edit;
|
||||
mod entity_commit;
|
||||
|
||||
@@ -16,6 +16,9 @@ pub(super) const BLOCK_SYSVARS: &[&str] = &[
|
||||
"BLOCKSTATE",
|
||||
"EXPLMODE",
|
||||
"INSNAME",
|
||||
"REFEDITNAME",
|
||||
"XEDIT",
|
||||
"XFADECTL",
|
||||
];
|
||||
|
||||
/// The `SETVAR ?` line for this family.
|
||||
@@ -45,6 +48,15 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_output(&format!("BLOCKSTATE = {state} (read only)"));
|
||||
}
|
||||
"REFEDITNAME" => {
|
||||
let name = self.tabs[i]
|
||||
.refedit_session
|
||||
.as_ref()
|
||||
.map(|s| s.block_name.clone())
|
||||
.unwrap_or_default();
|
||||
self.command_line
|
||||
.push_output(&format!("REFEDITNAME = \"{name}\" (read only)"));
|
||||
}
|
||||
"INSNAME" | "BLOCKNAVIGATE" => {
|
||||
let current = if name == "INSNAME" {
|
||||
self.insname.clone()
|
||||
@@ -77,6 +89,8 @@ impl OpenCADStudio {
|
||||
let (current, max) = match name.as_str() {
|
||||
"BLOCKMRULIST" => (self.block_mru_list as i32, 100),
|
||||
"BLOCKREDEFINEMODE" => (self.block_redefine_mode as i32, 2),
|
||||
"XEDIT" => (self.tabs[i].scene.document.header.xedit as i32, 1),
|
||||
"XFADECTL" => (crate::scene::cache::block_cache::refedit_fade_ctl(), 90),
|
||||
_ => (explmode(&self.tabs[i].scene.document) as i32, 1),
|
||||
};
|
||||
let ask = |app: &mut Self, name: String| {
|
||||
@@ -109,6 +123,23 @@ impl OpenCADStudio {
|
||||
self.block_redefine_mode = n as u8;
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
"XEDIT" => {
|
||||
if n != current {
|
||||
self.push_undo_snapshot(i, "XEDIT");
|
||||
self.tabs[i].scene.document.header.xedit = n == 1;
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
}
|
||||
"XFADECTL" => {
|
||||
crate::scene::cache::block_cache::set_refedit_fade_ctl(n);
|
||||
self.persist_settings_if_changed();
|
||||
for tab in &mut self.tabs {
|
||||
if !tab.is_start {
|
||||
tab.scene.recolor_meshes();
|
||||
tab.scene.bump_geometry();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if n != current {
|
||||
self.push_undo_snapshot(i, "EXPLMODE");
|
||||
|
||||
@@ -457,35 +457,6 @@ impl OpenCADStudio {
|
||||
self.open_attedit_dialog();
|
||||
}
|
||||
|
||||
// ── REFEDIT — in-place block editing ─────────────────────────────
|
||||
"REFEDIT" => {
|
||||
use crate::modules::draw::modify::refedit::RefEditPickCommand;
|
||||
// If a session is already active, tell the user.
|
||||
if self.tabs[i].refedit_session.is_some() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("REFEDIT: a session is already active. Use REFCLOSE first.")
|
||||
.as_ref(),
|
||||
);
|
||||
} else {
|
||||
// Check if a single INSERT is already selected.
|
||||
let selected: Vec<_> =
|
||||
self.tabs[i].scene.selected_entities().into_iter().collect();
|
||||
if selected.len() == 1 {
|
||||
if let Some(codec::EntityType::Insert(_)) = selected.first().map(|(_, e)| e)
|
||||
{
|
||||
let handle = selected[0].0;
|
||||
// Skip pick phase — jump straight to begin.
|
||||
let _ =
|
||||
self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value()));
|
||||
return Some(Task::none());
|
||||
}
|
||||
}
|
||||
let cmd_obj = RefEditPickCommand::new();
|
||||
self.command_line.push_info(&cmd_obj.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||
}
|
||||
}
|
||||
|
||||
"BEDIT" => {
|
||||
use crate::modules::draw::modify::block_edit::BlockEditPickCommand;
|
||||
if self.tabs[i].refedit_session.is_some() {
|
||||
@@ -668,303 +639,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
|
||||
use crate::modules::draw::modify::refedit::{
|
||||
apply_insert_transform, RefEditSession,
|
||||
};
|
||||
use codec::Handle;
|
||||
|
||||
let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0);
|
||||
let insert_handle = Handle::new(handle_u64);
|
||||
if self.reject_locked_edit(i, insert_handle) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
// Get INSERT entity.
|
||||
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
||||
Some(codec::EntityType::Insert(ins)) => ins.clone(),
|
||||
_ => {
|
||||
self.command_line.push_error(
|
||||
crate::t!("REFEDIT: selected object is not an INSERT.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
|
||||
// Build the INSERT's full placement transform (OCS + rotation +
|
||||
// scale, including non-uniform / mirrored, minus the block base
|
||||
// point) and its inverse, so edits round-trip back to
|
||||
// block-local coordinates on SAVE. The base point is part of
|
||||
// the placement: without it the working copy sits Rot*Scale*base
|
||||
// away from the reference the user picked.
|
||||
let sx = insert.x_scale();
|
||||
let sy = insert.y_scale();
|
||||
let sz = insert.z_scale();
|
||||
let base = crate::scene::render_graph::block_base_point(
|
||||
&self.tabs[i].scene.document,
|
||||
&insert.block_name,
|
||||
);
|
||||
let forward = crate::scene::render_graph::insert_transform_with_base(&insert, base);
|
||||
let inverse = {
|
||||
use codec::types::{Matrix3, Matrix4, Transform};
|
||||
let ocs_t =
|
||||
Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose());
|
||||
let t_inv = Matrix4::translation(
|
||||
-insert.insert_point.x,
|
||||
-insert.insert_point.y,
|
||||
-insert.insert_point.z,
|
||||
);
|
||||
let r_inv = Matrix4::rotation_z(-insert.rotation);
|
||||
let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz);
|
||||
// inverse(T(-base)·OCS·T·R·S) = T(+base)·S⁻¹·R⁻¹·T⁻¹·OCSᵀ
|
||||
Transform::from_matrix(
|
||||
Matrix4::translation(base.x, base.y, base.z)
|
||||
* (s_inv * r_inv * t_inv * ocs_t),
|
||||
)
|
||||
};
|
||||
|
||||
// Find the block record.
|
||||
let br_handle = match self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&insert.block_name)
|
||||
{
|
||||
Some(br) => br.handle,
|
||||
None => {
|
||||
self.command_line.push_error(
|
||||
crate::tf!("REFEDIT: block \"{}\" not found.", insert.block_name)
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
|
||||
// Collect block-local entities (skip structural Block/BlockEnd/AttDef).
|
||||
let block_entities: Vec<_> = {
|
||||
let br = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&insert.block_name)
|
||||
.unwrap();
|
||||
br.entity_handles
|
||||
.iter()
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||
.filter(|e| {
|
||||
!matches!(
|
||||
e,
|
||||
codec::EntityType::Block(_)
|
||||
| codec::EntityType::BlockEnd(_)
|
||||
| codec::EntityType::AttributeDefinition(_)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
if block_entities.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REFEDIT: block is empty.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
let session = RefEditSession {
|
||||
block_name: insert.block_name.clone(),
|
||||
br_handle,
|
||||
temp_handles: vec![],
|
||||
forward,
|
||||
inverse,
|
||||
// Everything allocated from here on is part of the session.
|
||||
handle_watermark: self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.next_handle()
|
||||
.max(self.tabs[i].scene.document.header.handle_seed),
|
||||
};
|
||||
|
||||
self.push_undo_snapshot(i, "REFEDIT");
|
||||
self.tabs[i].refedit_session = Some(session.clone());
|
||||
|
||||
// Add block entities to model space with INSERT transform applied.
|
||||
let mut temp_handles = Vec::new();
|
||||
for mut entity in block_entities {
|
||||
apply_insert_transform(&mut entity, &session);
|
||||
entity.common_mut().handle = codec::Handle::NULL;
|
||||
entity.common_mut().owner_handle = codec::Handle::NULL;
|
||||
let h = self.tabs[i].scene.add_entity(entity);
|
||||
temp_handles.push(h);
|
||||
}
|
||||
self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone();
|
||||
|
||||
// Fade everything except the entities being edited, so the
|
||||
// surrounding drawing stays visible for context but the block's
|
||||
// geometry stands out. (#136)
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_refedit_keep(Some(temp_handles.iter().copied().collect()));
|
||||
|
||||
// Select the temp entities so user can see what they're editing.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
for h in &temp_handles {
|
||||
self.tabs[i].scene.select_entity(*h, false);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
|
||||
// No active command — the user edits the block's geometry freely
|
||||
// (move, grips, draw, erase…) and runs REFCLOSE when done. (#136)
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.command_line.push_info(crate::tf!(
|
||||
"REFEDIT: Editing block \"{}\". Run REFCLOSE to save, REFCLOSE_DISCARD to cancel.",
|
||||
insert.block_name
|
||||
).as_ref());
|
||||
}
|
||||
|
||||
"REFCLOSE" => {
|
||||
if self.tabs[i].refedit_session.is_some() {
|
||||
use crate::modules::draw::modify::refedit::RefCloseCommand;
|
||||
let cmd_obj = RefCloseCommand::new();
|
||||
self.command_line.push_info(&cmd_obj.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
||||
} else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REFCLOSE: no REFEDIT session active.").as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
"REFCLOSE_SAVE" => {
|
||||
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
||||
use crate::modules::draw::modify::refedit::apply_insert_inverse_transform;
|
||||
|
||||
let session = match self.tabs[i].refedit_session.take() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REFCLOSE: no REFEDIT session active.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
|
||||
self.push_undo_snapshot(i, "REFCLOSE");
|
||||
|
||||
// The working set: the temp copies PLUS everything the user
|
||||
// created in this space during the session (drawn / pasted /
|
||||
// offset entities carry handles at or above the watermark) —
|
||||
// those must fold into the block too, not stay behind in
|
||||
// model space (#423).
|
||||
let working: Vec<codec::Handle> = {
|
||||
let space = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let drawn = self.tabs[i].scene.document.entities().filter(|e| {
|
||||
let c = e.common();
|
||||
c.owner_handle == space
|
||||
&& c.handle.value() >= session.handle_watermark
|
||||
&& !session.temp_handles.contains(&c.handle)
|
||||
});
|
||||
session
|
||||
.temp_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(drawn.map(|e| e.common().handle))
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Collect the edited temp entities.
|
||||
let new_entities: Vec<codec::EntityType> = working
|
||||
.iter()
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||
.collect();
|
||||
|
||||
// Remove temp entities from model space.
|
||||
self.tabs[i].scene.erase_entities(&working);
|
||||
|
||||
// Apply inverse INSERT transform → block-local coordinates.
|
||||
let new_entities: Vec<_> = new_entities
|
||||
.into_iter()
|
||||
.map(|mut entity| {
|
||||
apply_insert_inverse_transform(&mut entity, &session);
|
||||
let mut entity = normalize_entity_for_block(entity);
|
||||
entity.common_mut().handle = codec::Handle::NULL;
|
||||
entity.common_mut().owner_handle = session.br_handle;
|
||||
entity
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Remove old block entities from the document.
|
||||
let old_handles: Vec<_> = match self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&session.block_name)
|
||||
{
|
||||
Some(br) => br.entity_handles.clone(),
|
||||
None => vec![],
|
||||
};
|
||||
for h in &old_handles {
|
||||
self.tabs[i].scene.document.remove_entity(*h);
|
||||
}
|
||||
// Flush the entity_handles list from the block record.
|
||||
if let Some(br) = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get_mut(&session.block_name)
|
||||
{
|
||||
br.entity_handles.clear();
|
||||
}
|
||||
|
||||
// Add the new block entities.
|
||||
for entity in new_entities {
|
||||
let _ = self.tabs[i].scene.document.add_entity(entity);
|
||||
}
|
||||
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"REFCLOSE: Block \"{}\" saved. All references updated.",
|
||||
session.block_name
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
// End the edit fade before rebuilding, so the restored geometry
|
||||
// recolours bright. (#136)
|
||||
self.tabs[i].scene.set_refedit_keep(None);
|
||||
// Rebuild hatch/image/mesh caches since block content changed.
|
||||
self.tabs[i].scene.rebuild_derived_caches();
|
||||
}
|
||||
|
||||
"REFCLOSE_DISCARD" => {
|
||||
let session = match self.tabs[i].refedit_session.take() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REFCLOSE: no REFEDIT session active.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
// Remove the working set without modifying the block — the
|
||||
// temp copies plus anything created during the session (#423).
|
||||
let working: Vec<codec::Handle> = {
|
||||
let space = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let drawn = self.tabs[i].scene.document.entities().filter(|e| {
|
||||
let c = e.common();
|
||||
c.owner_handle == space
|
||||
&& c.handle.value() >= session.handle_watermark
|
||||
&& !session.temp_handles.contains(&c.handle)
|
||||
});
|
||||
session
|
||||
.temp_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(drawn.map(|e| e.common().handle))
|
||||
.collect()
|
||||
};
|
||||
self.tabs[i].scene.erase_entities(&working);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
// End the edit fade — restore the drawing to full brightness.
|
||||
self.tabs[i].scene.set_refedit_keep(None);
|
||||
self.command_line
|
||||
.push_output(crate::t!("REFCLOSE: Changes discarded.").as_ref());
|
||||
}
|
||||
|
||||
"ALIGN" => {
|
||||
use crate::modules::draw::modify::align::AlignCommand;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ mod pc_extract;
|
||||
mod pdf_dialogs;
|
||||
pub(crate) mod pdf_import;
|
||||
mod pdf_underlay;
|
||||
mod refedit;
|
||||
mod plotvars;
|
||||
mod point_cloud;
|
||||
pub(crate) mod sheet_set;
|
||||
@@ -331,6 +332,9 @@ impl OpenCADStudio {
|
||||
if let Some(t) = self.dispatch_dim(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_refedit(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_inquiry(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
//! REFEDIT, -REFEDIT, REFSET and REFCLOSE: in-place editing of a block
|
||||
//! reference or an external reference, with the reference's prompts and
|
||||
//! messages. The session itself lives in `DocumentTab::refedit_session`.
|
||||
|
||||
use crate::app::{Message, OpenCADStudio};
|
||||
use crate::modules::draw::modify::refedit::{
|
||||
apply_insert_inverse_transform, apply_insert_transform, level_hits,
|
||||
nesting_chain, RefCloseCommand, RefEditOptionsCommand, RefEditPickCommand, RefEditSession,
|
||||
RefSetCommand, NOT_CHECKED_OUT,
|
||||
};
|
||||
use codec::{EntityType, Handle};
|
||||
use iced::Task;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// `** Command not allowed, NAME already checked out for editing **`
|
||||
fn already_checked_out(name: &str) -> String {
|
||||
crate::tf!("** Command not allowed, {} already checked out for editing **", name).into_owned()
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_refedit(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
"REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")),
|
||||
"REFSET" => {
|
||||
if self.tabs[i].refedit_session.is_none() {
|
||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.command_line.push_info(
|
||||
crate::t!("Transfer objects between the RefEdit working set and host drawing...")
|
||||
.as_ref(),
|
||||
);
|
||||
let command = RefSetCommand::new();
|
||||
use crate::command::CadCommand;
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Some(Task::none())
|
||||
}
|
||||
"REFCLOSE" => {
|
||||
if self.tabs[i].refedit_session.is_none() {
|
||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
let command = RefCloseCommand::new();
|
||||
use crate::command::CadCommand;
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Some(Task::none())
|
||||
}
|
||||
"REFCLOSE_SAVE" => {
|
||||
self.close_refedit(i, true);
|
||||
Some(Task::none())
|
||||
}
|
||||
"REFCLOSE_DISCARD" => {
|
||||
self.close_refedit(i, false);
|
||||
Some(Task::none())
|
||||
}
|
||||
_ if cmd.starts_with("REFEDIT_PICKED:") => {
|
||||
self.refedit_picked(i, &cmd["REFEDIT_PICKED:".len()..]);
|
||||
Some(Task::none())
|
||||
}
|
||||
_ if cmd.starts_with("REFEDIT_BEGIN:") => {
|
||||
self.begin_refedit(i, &cmd["REFEDIT_BEGIN:".len()..]);
|
||||
Some(Task::none())
|
||||
}
|
||||
_ if cmd.starts_with("REFSET_APPLY:") => {
|
||||
self.apply_refset(i, cmd.ends_with("ADD"));
|
||||
Some(Task::none())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Block references and xrefs of the current space.
|
||||
fn refedit_references(&self, i: usize) -> HashSet<Handle> {
|
||||
let space = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| matches!(e, EntityType::Insert(_)) && e.common().owner_handle == space)
|
||||
.map(|e| e.common().handle)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn start_refedit(&mut self, i: usize, command_line: bool) -> Task<Message> {
|
||||
if let Some(session) = &self.tabs[i].refedit_session {
|
||||
let message = already_checked_out(&session.block_name);
|
||||
self.command_line.push_error(&message);
|
||||
return Task::none();
|
||||
}
|
||||
let references = self.refedit_references(i);
|
||||
// A single preselected reference is taken as the pick.
|
||||
let selected: Vec<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
if let [handle] = selected[..] {
|
||||
if references.contains(&handle) {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.refedit_picked(
|
||||
i,
|
||||
&format!("{}:{}", command_line as u8, handle.value()),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
use crate::command::CadCommand;
|
||||
let command = RefEditPickCommand::new(command_line, references);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// `cli:handle[:x:y:z]` — the reference was picked (at the point).
|
||||
fn refedit_picked(&mut self, i: usize, args: &str) {
|
||||
let parts: Vec<&str> = args.split(':').collect();
|
||||
let command_line = parts.first() == Some(&"1");
|
||||
let Some(top) = parts.get(1).and_then(|v| v.parse::<u64>().ok()).map(Handle::new) else {
|
||||
return;
|
||||
};
|
||||
let point = (parts.len() >= 5)
|
||||
.then(|| {
|
||||
Some(glam::DVec3::new(
|
||||
parts[2].parse().ok()?,
|
||||
parts[3].parse().ok()?,
|
||||
parts[4].parse().ok()?,
|
||||
))
|
||||
})
|
||||
.flatten();
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let chain = nesting_chain(&self.tabs[i].scene.document, top, point);
|
||||
if chain.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Object is not a valid reference").as_ref());
|
||||
return;
|
||||
}
|
||||
if command_line {
|
||||
let hits = chain
|
||||
.iter()
|
||||
.map(|level| level_hits(&self.tabs[i].scene.document, level))
|
||||
.collect();
|
||||
let command = RefEditOptionsCommand::new(top, chain, hits);
|
||||
use crate::command::CadCommand;
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
} else {
|
||||
// The dialog's defaults: the innermost reference, all objects,
|
||||
// attribute definitions not displayed.
|
||||
let path: Vec<String> = chain[1..]
|
||||
.iter()
|
||||
.map(|l| l.insert.value().to_string())
|
||||
.collect();
|
||||
let _ = self.dispatch_command(&format!(
|
||||
"REFEDIT_BEGIN:{}:{}:0:",
|
||||
top.value(),
|
||||
path.join(",")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// `top[:path[:attributes[:nested]]]` — open the session.
|
||||
fn begin_refedit(&mut self, i: usize, args: &str) {
|
||||
let mut parts = args.split(':');
|
||||
let Some(top) = parts
|
||||
.next()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Handle::new)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let handles = |s: Option<&str>| -> Vec<Handle> {
|
||||
s.unwrap_or("")
|
||||
.split(',')
|
||||
.filter_map(|v| v.parse::<u64>().ok())
|
||||
.map(Handle::new)
|
||||
.collect()
|
||||
};
|
||||
let path = handles(parts.next());
|
||||
let attributes = parts.next() == Some("1");
|
||||
let nested = handles(parts.next());
|
||||
if self.tabs[i].refedit_session.is_some() || self.reject_locked_edit(i, top) {
|
||||
return;
|
||||
}
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let mut chain = nesting_chain(document, top, None);
|
||||
if chain.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Object is not a valid reference").as_ref());
|
||||
return;
|
||||
}
|
||||
// Follow the stored path of definition-owned references.
|
||||
for handle in path {
|
||||
let Some(EntityType::Insert(inner)) = document.get_entity(handle) else {
|
||||
break;
|
||||
};
|
||||
let base = crate::scene::render_graph::block_base_point(document, &inner.block_name);
|
||||
let local = crate::scene::render_graph::insert_transform_with_base(inner, base);
|
||||
let outer = chain.last().expect("non-empty chain").forward.clone();
|
||||
chain.push(crate::modules::draw::modify::refedit::ChainLevel {
|
||||
insert: handle,
|
||||
block_name: inner.block_name.clone(),
|
||||
forward: local.then(&outer),
|
||||
});
|
||||
}
|
||||
let level = chain.last().expect("non-empty chain").clone();
|
||||
let Some(br) = document.block_records.get(&level.block_name) else {
|
||||
return;
|
||||
};
|
||||
let br_handle = br.handle;
|
||||
let xref_path = (br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.then(|| br.xref_path.clone())
|
||||
.filter(|p| !p.is_empty());
|
||||
let extracted: Vec<Handle> = br
|
||||
.entity_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|h| match document.get_entity(*h) {
|
||||
Some(EntityType::Block(_) | EntityType::BlockEnd(_)) | None => false,
|
||||
Some(EntityType::AttributeDefinition(_)) => attributes,
|
||||
Some(_) => nested.is_empty() || nested.contains(h),
|
||||
})
|
||||
.collect();
|
||||
let Some(inverse) =
|
||||
crate::modules::draw::modify::refedit::inverse_placement(&level.forward)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let session = RefEditSession {
|
||||
block_name: level.block_name.clone(),
|
||||
br_handle,
|
||||
temp_handles: Vec::new(),
|
||||
extracted: extracted.clone(),
|
||||
added: HashSet::new(),
|
||||
removed: HashSet::new(),
|
||||
forward: level.forward.clone(),
|
||||
inverse,
|
||||
// Everything allocated from here on is part of the session.
|
||||
handle_watermark: document.next_handle().max(document.header.handle_seed),
|
||||
xref_path,
|
||||
};
|
||||
let copies: Vec<EntityType> = extracted
|
||||
.iter()
|
||||
.filter_map(|h| document.get_entity(*h).cloned())
|
||||
.collect();
|
||||
|
||||
self.push_undo_snapshot(i, "REFEDIT");
|
||||
let mut temp_handles = Vec::new();
|
||||
for mut entity in copies {
|
||||
apply_insert_transform(&mut entity, &session);
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
entity.common_mut().owner_handle = Handle::NULL;
|
||||
temp_handles.push(self.tabs[i].scene.add_entity(entity));
|
||||
}
|
||||
let mut session = session;
|
||||
session.temp_handles = temp_handles.clone();
|
||||
self.tabs[i].refedit_session = Some(session);
|
||||
self.tabs[i].refedit_closed.clear();
|
||||
// Fade everything outside the working set by XFADECTL. (#136)
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_refedit_keep(Some(temp_handles.iter().copied().collect::<rustc_hash::FxHashSet<_>>()));
|
||||
self.tabs[i].scene.refedit_lock = true;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].dirty = true;
|
||||
// No active command — the definition's objects are edited freely
|
||||
// (move, grips, draw, erase…) until REFCLOSE.
|
||||
self.tabs[i].active_cmd = None;
|
||||
}
|
||||
|
||||
/// REFSET Add / Remove on the current selection.
|
||||
fn apply_refset(&mut self, i: usize, add: bool) {
|
||||
let Some(mut session) = self.tabs[i].refedit_session.take() else {
|
||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||
return;
|
||||
};
|
||||
let picked: Vec<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
let mut moved = 0usize;
|
||||
for h in picked {
|
||||
let inside = session.in_working_set(h);
|
||||
if add && !inside {
|
||||
session.removed.remove(&h);
|
||||
if !session.in_working_set(h) {
|
||||
session.added.insert(h);
|
||||
}
|
||||
moved += 1;
|
||||
} else if !add && inside {
|
||||
session.added.remove(&h);
|
||||
if session.in_working_set(h) {
|
||||
session.removed.insert(h);
|
||||
}
|
||||
moved += 1;
|
||||
}
|
||||
}
|
||||
let keep: rustc_hash::FxHashSet<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|e| e.common().handle)
|
||||
.filter(|h| session.in_working_set(*h))
|
||||
.collect();
|
||||
self.tabs[i].refedit_session = Some(session);
|
||||
self.tabs[i].scene.set_refedit_keep(Some(keep));
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.command_line.push_output(&if add {
|
||||
crate::tf!("{} Added to working set", moved).into_owned()
|
||||
} else {
|
||||
crate::tf!("{} Removed from working set", moved).into_owned()
|
||||
});
|
||||
}
|
||||
|
||||
/// REFCLOSE Save (`save`) or Discard.
|
||||
fn close_refedit(&mut self, i: usize, save: bool) {
|
||||
let Some(session) = self.tabs[i].refedit_session.take() else {
|
||||
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
|
||||
return;
|
||||
};
|
||||
self.push_undo_snapshot(i, "REFCLOSE");
|
||||
self.command_line
|
||||
.push_output(crate::t!("Regenerating model.").as_ref());
|
||||
let space = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
// The working set: the copies still in it, everything created in this
|
||||
// space during the session (#423) and the host objects added with
|
||||
// REFSET — less what REFSET removed.
|
||||
let created: Vec<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
let c = e.common();
|
||||
c.owner_handle == space && c.handle.value() >= session.handle_watermark
|
||||
})
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
let working: Vec<Handle> = created
|
||||
.iter()
|
||||
.chain(session.added.iter())
|
||||
.copied()
|
||||
.filter(|h| session.in_working_set(*h))
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
if !save {
|
||||
// The copies and new objects go; host objects added with REFSET
|
||||
// stay where they are, and copies moved out with REFSET Remove
|
||||
// stay in the host.
|
||||
let drop: Vec<Handle> = working
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|h| !session.added.contains(h))
|
||||
.collect();
|
||||
self.tabs[i].scene.erase_entities(&drop);
|
||||
self.finish_refedit_close(i, session);
|
||||
return;
|
||||
}
|
||||
|
||||
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
||||
let originals: HashSet<Handle> = session.temp_handles.iter().copied().collect();
|
||||
let added_count = working.iter().filter(|h| !originals.contains(h)).count();
|
||||
let removed_count = session.extracted.len()
|
||||
- working.iter().filter(|h| originals.contains(h)).count();
|
||||
let edited: Vec<EntityType> = working
|
||||
.iter()
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||
.map(|mut entity| {
|
||||
apply_insert_inverse_transform(&mut entity, &session);
|
||||
let mut entity = normalize_entity_for_block(entity);
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
entity.common_mut().owner_handle = session.br_handle;
|
||||
entity
|
||||
})
|
||||
.collect();
|
||||
self.tabs[i].scene.erase_entities(&working);
|
||||
// The extracted definition objects are replaced; the rest stay.
|
||||
for h in &session.extracted {
|
||||
self.tabs[i].scene.document.remove_entity(*h);
|
||||
}
|
||||
if let Some(br) = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get_mut(&session.block_name)
|
||||
{
|
||||
let extracted: HashSet<Handle> = session.extracted.iter().copied().collect();
|
||||
br.entity_handles.retain(|h| !extracted.contains(h));
|
||||
}
|
||||
for entity in edited.iter().cloned() {
|
||||
let _ = self.tabs[i].scene.document.add_entity(entity);
|
||||
}
|
||||
let instances = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
matches!(e, EntityType::Insert(ins)
|
||||
if ins.block_name.eq_ignore_ascii_case(&session.block_name))
|
||||
})
|
||||
.count();
|
||||
let name = session.block_name.clone();
|
||||
match added_count {
|
||||
0 => {}
|
||||
1 => self
|
||||
.command_line
|
||||
.push_output(crate::tf!("1 object added to {}", name).as_ref()),
|
||||
n => self
|
||||
.command_line
|
||||
.push_output(crate::tf!("{} objects added to {}", n, name).as_ref()),
|
||||
}
|
||||
match removed_count {
|
||||
0 => {}
|
||||
1 => self
|
||||
.command_line
|
||||
.push_output(crate::tf!("1 object removed from {}", name).as_ref()),
|
||||
n => self
|
||||
.command_line
|
||||
.push_output(crate::tf!("{} objects removed from {}", n, name).as_ref()),
|
||||
}
|
||||
let reloaded = match &session.xref_path {
|
||||
Some(raw) => match self.write_back_xref(i, &session, raw, edited) {
|
||||
Ok(()) => true,
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
false
|
||||
}
|
||||
},
|
||||
None => false,
|
||||
};
|
||||
let updated = match (session.xref_path.is_some(), instances) {
|
||||
(true, 1) => crate::t!("1 xref instance updated").into_owned(),
|
||||
(true, n) => crate::tf!("{} xref instances updated", n).into_owned(),
|
||||
(false, 1) => crate::t!("1 block instance updated").into_owned(),
|
||||
(false, n) => crate::tf!("{} block instances updated", n).into_owned(),
|
||||
};
|
||||
self.command_line.push_output(&updated);
|
||||
self.command_line.push_output(&if reloaded {
|
||||
crate::tf!("{} redefined and reloaded.", name).into_owned()
|
||||
} else {
|
||||
crate::tf!("{} redefined.", name).into_owned()
|
||||
});
|
||||
self.finish_refedit_close(i, session);
|
||||
// Rebuild hatch/image/mesh caches since block content changed.
|
||||
self.tabs[i].scene.rebuild_derived_caches();
|
||||
}
|
||||
|
||||
fn finish_refedit_close(&mut self, i: usize, session: RefEditSession) {
|
||||
self.tabs[i].refedit_closed.push(session);
|
||||
self.tabs[i].scene.refedit_lock = false;
|
||||
self.tabs[i].scene.set_refedit_keep(None);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
|
||||
/// Write the edited definition into the referenced drawing and reload it.
|
||||
fn write_back_xref(
|
||||
&mut self,
|
||||
i: usize,
|
||||
session: &RefEditSession,
|
||||
raw: &str,
|
||||
edited: Vec<EntityType>,
|
||||
) -> Result<(), String> {
|
||||
let base_dir = self.tabs[i]
|
||||
.current_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
|
||||
.unwrap_or_default();
|
||||
let path = crate::io::xref::resolve_path(raw, &base_dir)
|
||||
.ok_or_else(|| format!("\"{raw}\" cannot be found."))?;
|
||||
let mut source = crate::io::load_file(&path)?;
|
||||
let prefix = format!("{}|", session.block_name);
|
||||
let strip = |name: &str| -> String {
|
||||
if name.len() > prefix.len()
|
||||
&& name.is_char_boundary(prefix.len())
|
||||
&& name[..prefix.len()].eq_ignore_ascii_case(&prefix)
|
||||
{
|
||||
name[prefix.len()..].to_string()
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
};
|
||||
// Dependencies come from the host (where they carry the xref prefix)
|
||||
// and go back under their own names.
|
||||
let mut deps = crate::app::ClipboardDeps::capture(&self.tabs[i].scene.document, &edited);
|
||||
{
|
||||
use codec::TableEntry;
|
||||
for layer in &mut deps.layers {
|
||||
let name = strip(layer.name());
|
||||
layer.name = name;
|
||||
}
|
||||
for linetype in &mut deps.linetypes {
|
||||
let name = strip(linetype.name());
|
||||
linetype.name = name;
|
||||
}
|
||||
for style in &mut deps.text_styles {
|
||||
let name = strip(style.name());
|
||||
style.name = name;
|
||||
}
|
||||
}
|
||||
for block in &mut deps.blocks {
|
||||
block.name = strip(&block.name);
|
||||
}
|
||||
let entities: Vec<EntityType> = edited
|
||||
.into_iter()
|
||||
.map(|mut entity| {
|
||||
let common = entity.common_mut();
|
||||
common.layer = strip(&common.layer);
|
||||
common.linetype = strip(&common.linetype);
|
||||
common.layer_handle = None;
|
||||
common.linetype_handle = None;
|
||||
common.owner_handle = Handle::NULL;
|
||||
match &mut entity {
|
||||
EntityType::Insert(insert) => insert.block_name = strip(&insert.block_name),
|
||||
EntityType::Text(text) => text.style = strip(&text.style),
|
||||
EntityType::MText(text) => text.style = strip(&text.style),
|
||||
_ => {}
|
||||
}
|
||||
entity
|
||||
})
|
||||
.collect();
|
||||
let model: Vec<Handle> = source
|
||||
.model_space_entities()
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
for h in model {
|
||||
source.remove_entity(h);
|
||||
}
|
||||
crate::app::command_driver::clipboard::paste_entities_kernel(
|
||||
&mut source,
|
||||
&entities,
|
||||
&deps,
|
||||
None,
|
||||
crate::app::command_driver::clipboard::PasteSpace::Model,
|
||||
);
|
||||
crate::io::save(&source, &path)?;
|
||||
let keys: rustc_hash::FxHashSet<Handle> = [session.br_handle].into_iter().collect();
|
||||
self.tabs[i].xref_stat_cache.0.remove(&session.br_handle.value());
|
||||
let _ = crate::io::xref::resolve_xrefs_for_keys(
|
||||
&mut self.tabs[i].scene.document,
|
||||
&base_dir,
|
||||
&keys,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Undo of REFCLOSE checks the reference out again; undo of REFEDIT ends
|
||||
/// the session (their redo does the reverse).
|
||||
pub(in crate::app) fn refedit_after_history_step(&mut self, i: usize, label: &str, undo: bool) {
|
||||
match (label, undo) {
|
||||
("REFCLOSE", true) | ("REFEDIT", false) => {
|
||||
let reopened = if undo {
|
||||
self.tabs[i].refedit_closed.pop()
|
||||
} else {
|
||||
self.tabs[i].refedit_reopen.pop()
|
||||
};
|
||||
if let Some(session) = reopened {
|
||||
let keep: rustc_hash::FxHashSet<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|e| e.common().handle)
|
||||
.filter(|h| session.in_working_set(*h))
|
||||
.collect();
|
||||
self.tabs[i].refedit_session = Some(session);
|
||||
self.tabs[i].scene.refedit_lock = true;
|
||||
self.tabs[i].scene.set_refedit_keep(Some(keep));
|
||||
}
|
||||
}
|
||||
("REFCLOSE", false) | ("REFEDIT", true) => {
|
||||
if let Some(session) = self.tabs[i].refedit_session.take() {
|
||||
if undo {
|
||||
self.tabs[i].refedit_reopen.push(session);
|
||||
} else {
|
||||
self.tabs[i].refedit_closed.push(session);
|
||||
}
|
||||
self.tabs[i].scene.refedit_lock = false;
|
||||
self.tabs[i].scene.set_refedit_keep(None);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,10 @@ pub(super) struct DocumentTab {
|
||||
pub(super) paper_bg_color: Option<[f32; 4]>,
|
||||
/// Active REFEDIT session, if any.
|
||||
pub(super) refedit_session: Option<RefEditSession>,
|
||||
/// Sessions closed by REFCLOSE, reopened by undoing it.
|
||||
pub(super) refedit_closed: Vec<RefEditSession>,
|
||||
/// Sessions ended by undoing REFEDIT, reopened by redoing it.
|
||||
pub(super) refedit_reopen: Vec<RefEditSession>,
|
||||
/// COUNT mode, while it is on.
|
||||
pub(crate) count: Option<crate::ui::window::count_palette::CountMode>,
|
||||
/// Open BEDIT block tabs. Definitions are edited live; each tab owns its
|
||||
@@ -665,6 +669,8 @@ impl DocumentTab {
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
refedit_session: None,
|
||||
refedit_closed: Vec::new(),
|
||||
refedit_reopen: Vec::new(),
|
||||
count: None,
|
||||
block_edits: Vec::new(),
|
||||
active_block_edit: None,
|
||||
|
||||
@@ -1208,6 +1208,7 @@ impl OpenCADStudio {
|
||||
break;
|
||||
};
|
||||
last_label = snapshot.label().to_string();
|
||||
let step_label = last_label.clone();
|
||||
match snapshot {
|
||||
HistorySnapshot::Delta(mut d) => {
|
||||
// Symmetric: undo applies the before side, then the same
|
||||
@@ -1242,6 +1243,7 @@ impl OpenCADStudio {
|
||||
.push(HistorySnapshot::ObjectVisibility(v));
|
||||
}
|
||||
}
|
||||
self.refedit_after_history_step(i, &step_label, true);
|
||||
}
|
||||
self.finish_history_apply(i, had_full, layer_panel_changed, &changes);
|
||||
self.command_line
|
||||
@@ -1268,6 +1270,7 @@ impl OpenCADStudio {
|
||||
break;
|
||||
};
|
||||
last_label = snapshot.label().to_string();
|
||||
let step_label = last_label.clone();
|
||||
match snapshot {
|
||||
HistorySnapshot::Delta(mut d) => {
|
||||
layer_panel_changed |= d.structure.as_ref().is_some_and(|structure| {
|
||||
@@ -1299,6 +1302,7 @@ impl OpenCADStudio {
|
||||
.push(HistorySnapshot::ObjectVisibility(v));
|
||||
}
|
||||
}
|
||||
self.refedit_after_history_step(i, &step_label, false);
|
||||
}
|
||||
self.finish_history_apply(i, had_full, layer_panel_changed, &changes);
|
||||
self.command_line
|
||||
|
||||
@@ -652,6 +652,9 @@ pub struct UserSettings {
|
||||
/// BLOCKNAVIGATE (default ".").
|
||||
#[serde(default = "default_block_navigate")]
|
||||
pub block_navigate: String,
|
||||
/// XFADECTL: how far objects outside the REFEDIT working set fade (0–90, default 70).
|
||||
#[serde(default = "default_xfadectl")]
|
||||
pub xfadectl: u8,
|
||||
/// COUNTCOLOR (default 3).
|
||||
#[serde(default = "default_count_color")]
|
||||
pub count_color: i16,
|
||||
@@ -722,6 +725,10 @@ fn default_block_view() -> u8 {
|
||||
1
|
||||
}
|
||||
|
||||
fn default_xfadectl() -> u8 {
|
||||
70
|
||||
}
|
||||
|
||||
fn default_block_mru_list() -> u8 {
|
||||
50
|
||||
}
|
||||
@@ -931,6 +938,7 @@ impl Default for UserSettings {
|
||||
block_mru_list: 50,
|
||||
block_redefine_mode: 1,
|
||||
block_navigate: ".".to_string(),
|
||||
xfadectl: 70,
|
||||
count_color: 3,
|
||||
count_error_color: 1,
|
||||
count_service: true,
|
||||
|
||||
@@ -873,6 +873,7 @@ impl OpenCADStudio {
|
||||
block_mru_list: self.block_mru_list,
|
||||
block_redefine_mode: self.block_redefine_mode,
|
||||
block_navigate: self.block_navigate.clone(),
|
||||
xfadectl: crate::scene::cache::block_cache::refedit_fade_ctl() as u8,
|
||||
count_color: self.count_palette.color,
|
||||
count_error_color: self.count_palette.error_color,
|
||||
count_service: self.count_palette.service,
|
||||
@@ -1034,6 +1035,7 @@ impl OpenCADStudio {
|
||||
self.block_mru_list = s.block_mru_list.min(100);
|
||||
self.block_redefine_mode = s.block_redefine_mode.min(2);
|
||||
self.block_navigate = s.block_navigate.clone();
|
||||
crate::scene::cache::block_cache::set_refedit_fade_ctl(i32::from(s.xfadectl.min(90)));
|
||||
self.count_palette.color = s.count_color;
|
||||
self.count_palette.error_color = s.count_error_color;
|
||||
self.count_palette.service = s.count_service;
|
||||
|
||||
+2
-2
@@ -654,14 +654,14 @@ pub fn layer_overrides(doc: &CadDocument, name: &str, raw_path: &str, base_dir:
|
||||
/// to the name-keyed bytes registry instead (trailing-name match); the
|
||||
/// returned path is the registry key.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn resolve_path(raw: &str, _base_dir: &Path) -> Option<PathBuf> {
|
||||
pub(crate) fn resolve_path(raw: &str, _base_dir: &Path) -> Option<PathBuf> {
|
||||
find_registered_source(raw).map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// Try to build an absolute path from a raw xref path string.
|
||||
/// Handles absolute paths, relative paths, and Windows-style separators.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
||||
pub(crate) fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
||||
let normalised = raw.replace('\\', "/");
|
||||
let p = PathBuf::from(&normalised);
|
||||
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
|
||||
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
|
||||
match source {
|
||||
"Select reference:" => Some(("ref-edit", "select-reference")),
|
||||
"Reference not found." => Some(("ref-edit", "reference-not-found")),
|
||||
"Object is not a valid reference" => Some(("ref-edit", "not-valid-reference")),
|
||||
"1 entity added" => Some(("ref-edit", "one-entity-added")),
|
||||
"Select nesting level [Ok/Next] <Next>:" => Some(("ref-edit", "nesting-level")),
|
||||
"Enter object selection method [All/Nested] <All>:" => Some(("ref-edit", "selection-method")),
|
||||
"Select nested objects:" => Some(("ref-edit", "select-nested")),
|
||||
"Display attribute definitions [Yes/No] <No>:" => Some(("ref-edit", "display-attdefs")),
|
||||
"0 items selected" => Some(("ref-edit", "zero-items-selected")),
|
||||
"1 item selected" => Some(("ref-edit", "one-item-selected")),
|
||||
"{} items selected" => Some(("ref-edit", "items-selected")),
|
||||
"Enter an option [Add/Remove] <Add>:" => Some(("ref-edit", "refset-option")),
|
||||
"Enter option [Save/Discard reference changes] <Save>:" => Some(("ref-edit", "refclose-option")),
|
||||
"** Command not allowed unless a reference is checked out with REFEDIT command **" => Some(("ref-edit", "not-checked-out")),
|
||||
"** Command not allowed, {} already checked out for editing **" => Some(("ref-edit", "already-checked-out")),
|
||||
"Transfer objects between the RefEdit working set and host drawing..." => Some(("ref-edit", "transfer-objects")),
|
||||
"{} Added to working set" => Some(("ref-edit", "added-to-working-set")),
|
||||
"{} Removed from working set" => Some(("ref-edit", "removed-from-working-set")),
|
||||
"1 object added to {}" => Some(("ref-edit", "one-object-added")),
|
||||
"{} objects added to {}" => Some(("ref-edit", "objects-added")),
|
||||
"1 object removed from {}" => Some(("ref-edit", "one-object-removed")),
|
||||
"{} objects removed from {}" => Some(("ref-edit", "objects-removed")),
|
||||
"1 xref instance updated" => Some(("ref-edit", "one-xref-instance")),
|
||||
"{} xref instances updated" => Some(("ref-edit", "xref-instances")),
|
||||
"1 block instance updated" => Some(("ref-edit", "one-block-instance")),
|
||||
"{} block instances updated" => Some(("ref-edit", "block-instances")),
|
||||
"{} redefined and reloaded." => Some(("ref-edit", "redefined-reloaded")),
|
||||
"{} redefined." => Some(("ref-edit", "redefined")),
|
||||
"Ok" => Some(("ref-edit", "ok")),
|
||||
"Nested" => Some(("ref-edit", "nested")),
|
||||
"Operational Symbols" => Some(("property-patch", "patch-07")),
|
||||
"Decimal Separator" => Some(("property-patch", "patch-06")),
|
||||
"Open 3Dconnexion settings..." => Some(("property-patch", "patch-05")),
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// REFEDIT — in-place block reference editing.
|
||||
// REFEDIT / -REFEDIT / REFSET / REFCLOSE — in-place reference editing.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. REFEDIT: user picks an INSERT entity.
|
||||
// 2. The block's entities are copied into model space with the INSERT
|
||||
// transform applied (translate + rotate + uniform scale).
|
||||
// 3. The user edits them with normal commands (MOVE, COPY, DELETE, …).
|
||||
// 4. REFCLOSE SAVE: temp entities are inverse-transformed back into the
|
||||
// block definition; all INSERT references auto-update.
|
||||
// REFCLOSE DISCARD: temp entities are removed, block unchanged.
|
||||
//
|
||||
// Limitation: non-uniform scale inserts (x_scale ≠ y_scale) are rejected
|
||||
// with an error message — full matrix inversion for those cases would
|
||||
// require per-entity matrix transforms not yet in EntityTransform.
|
||||
// 1. REFEDIT (or -REFEDIT on the command line) picks a block reference or
|
||||
// xref; the nesting chain under the pick is offered innermost first and
|
||||
// `Next` steps outward.
|
||||
// 2. The chosen definition's objects (all of them, or the nested objects
|
||||
// picked) are copied into the current space with the composed placement
|
||||
// of the chain. Everything else fades by XFADECTL.
|
||||
// 3. The user edits them with normal commands; REFSET moves host objects
|
||||
// into the working set or working-set objects out to the host.
|
||||
// 4. REFCLOSE Save writes the working set back into the definition (and
|
||||
// into the referenced file for an xref); Discard drops the copies.
|
||||
|
||||
use crate::t;
|
||||
use codec::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
/// Right-edge side-toolbar buttons shown while a REFEDIT session is active:
|
||||
@@ -39,22 +39,33 @@ pub fn refedit_tools() -> Vec<ToolDef> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Printed by REFSET and REFCLOSE outside a session.
|
||||
pub const NOT_CHECKED_OUT: &str =
|
||||
"** Command not allowed unless a reference is checked out with REFEDIT command **";
|
||||
|
||||
// ── Session state (held in DocumentTab) ───────────────────────────────────
|
||||
|
||||
/// Active REFEDIT session. Lives in `DocumentTab::refedit_session`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefEditSession {
|
||||
/// Name of the block being edited.
|
||||
/// Name of the definition being edited (REFEDITNAME).
|
||||
pub block_name: String,
|
||||
/// Handle of the block record (owns the block entities).
|
||||
pub br_handle: Handle,
|
||||
/// Handles of the temporary model-space entities added for editing.
|
||||
/// The working copies of the extracted definition objects.
|
||||
pub temp_handles: Vec<Handle>,
|
||||
// ── INSERT placement, as full affine transforms ───────────────────
|
||||
/// Block-local → world (the INSERT's `get_transform`). Handles OCS,
|
||||
/// rotation and non-uniform / mirrored scale in one matrix.
|
||||
/// The definition objects that were copied out, index-aligned with
|
||||
/// `temp_handles`. Objects not extracted (attribute definitions when they
|
||||
/// are not displayed, objects not picked in Nested mode) stay in the
|
||||
/// definition untouched.
|
||||
pub extracted: Vec<Handle>,
|
||||
/// Host objects moved into the working set with REFSET Add.
|
||||
pub added: HashSet<Handle>,
|
||||
/// Working-set objects moved out to the host with REFSET Remove.
|
||||
pub removed: HashSet<Handle>,
|
||||
/// Definition-local → world: the composed placement of the chain.
|
||||
pub forward: codec::types::Transform,
|
||||
/// World → block-local, applied on SAVE to bring edits back.
|
||||
/// World → definition-local, applied on SAVE to bring edits back.
|
||||
pub inverse: codec::types::Transform,
|
||||
/// Handle watermark captured before the temp copies were created: every
|
||||
/// entity allocated at or above it during the session (drawn, pasted,
|
||||
@@ -62,51 +73,482 @@ pub struct RefEditSession {
|
||||
/// and Discard removes it (#423). Catches every creation path without
|
||||
/// per-command hooks.
|
||||
pub handle_watermark: u64,
|
||||
/// The referenced file when the definition is an xref.
|
||||
pub xref_path: Option<String>,
|
||||
}
|
||||
|
||||
// ── REFEDIT pick command ───────────────────────────────────────────────────
|
||||
impl RefEditSession {
|
||||
/// Whether `handle` (a host-space object) is in the working set.
|
||||
pub fn in_working_set(&self, handle: Handle) -> bool {
|
||||
if self.removed.contains(&handle) {
|
||||
return false;
|
||||
}
|
||||
self.temp_handles.contains(&handle)
|
||||
|| self.added.contains(&handle)
|
||||
|| handle.value() >= self.handle_watermark
|
||||
}
|
||||
}
|
||||
|
||||
/// Step 1: wait for the user to pick a single INSERT entity.
|
||||
pub struct RefEditPickCommand;
|
||||
// ── Nesting chain under a pick ────────────────────────────────────────────
|
||||
|
||||
/// One level of the nesting chain: the reference (top-level object handle for
|
||||
/// level 0, the definition-owned INSERT handle below it) and its definition.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainLevel {
|
||||
pub insert: Handle,
|
||||
pub block_name: String,
|
||||
/// Definition-local → world for this level.
|
||||
pub forward: codec::types::Transform,
|
||||
}
|
||||
|
||||
/// The pickable objects of one definition, in world space, for Nested
|
||||
/// selection: (definition-owned handle, polyline paths of its leaves).
|
||||
pub type LevelHits = Vec<(Handle, Vec<Vec<[f64; 3]>>)>;
|
||||
|
||||
/// World paths of an entity for hit-testing (curves sampled by the renderer;
|
||||
/// block references recurse into their definitions).
|
||||
pub fn hit_paths(
|
||||
entity: &EntityType,
|
||||
document: &codec::CadDocument,
|
||||
depth: usize,
|
||||
) -> Vec<Vec<[f64; 3]>> {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
use crate::scene::convert::acad_to_render::RenderObject;
|
||||
if let EntityType::Insert(insert) = entity {
|
||||
if depth > 8 {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(br) = document.block_records.get(&insert.block_name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let base = crate::scene::render_graph::block_base_point(document, &insert.block_name);
|
||||
let placement = crate::scene::render_graph::insert_transform_with_base(insert, base);
|
||||
return br
|
||||
.entity_handles
|
||||
.iter()
|
||||
.filter_map(|h| document.get_entity(*h))
|
||||
.filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)))
|
||||
.flat_map(|child| {
|
||||
let mut child = child.clone();
|
||||
child.as_entity_mut().apply_transform(&placement);
|
||||
hit_paths(&child, document, depth + 1)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
match entity
|
||||
.to_render_entity(document)
|
||||
.map(|render| render.object)
|
||||
{
|
||||
Some(RenderObject::Dot(point)) => vec![vec![point]],
|
||||
Some(
|
||||
RenderObject::Lines(points)
|
||||
| RenderObject::SegmentedLines(points)
|
||||
| RenderObject::TaperedLines(points, _)
|
||||
| RenderObject::BoundaryLines { points, .. },
|
||||
) => vec![points],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance from `point` to a set of paths.
|
||||
pub fn paths_distance(paths: &[Vec<[f64; 3]>], point: DVec3) -> Option<f64> {
|
||||
let p = kernel::space::Vec3::from(point.to_array());
|
||||
paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
if path.len() == 1 {
|
||||
return Some(p.distance(kernel::space::Vec3::from(path[0])));
|
||||
}
|
||||
path.windows(2)
|
||||
.filter(|s| s.iter().flatten().all(|v| v.is_finite()))
|
||||
.map(|s| p.distance_to_segment(s[0].into(), s[1].into()))
|
||||
.min_by(f64::total_cmp)
|
||||
})
|
||||
.min_by(f64::total_cmp)
|
||||
}
|
||||
|
||||
/// The world-space objects of `level`'s definition with their hit paths.
|
||||
pub fn level_hits(document: &codec::CadDocument, level: &ChainLevel) -> LevelHits {
|
||||
let Some(br) = document.block_records.get(&level.block_name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
br.entity_handles
|
||||
.iter()
|
||||
.filter_map(|h| document.get_entity(*h).map(|e| (*h, e)))
|
||||
.filter(|(_, e)| {
|
||||
!matches!(
|
||||
e,
|
||||
EntityType::Block(_) | EntityType::BlockEnd(_) | EntityType::AttributeDefinition(_)
|
||||
)
|
||||
})
|
||||
.map(|(h, e)| {
|
||||
let mut world = e.clone();
|
||||
world.as_entity_mut().apply_transform(&level.forward);
|
||||
(h, hit_paths(&world, document, 0))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The nesting chain of `top` under `point`, outermost first: the top-level
|
||||
/// reference, then every definition-owned reference the nearest object under
|
||||
/// the point sits in. Without a point only the top level is returned.
|
||||
pub fn nesting_chain(
|
||||
document: &codec::CadDocument,
|
||||
top: Handle,
|
||||
point: Option<DVec3>,
|
||||
) -> Vec<ChainLevel> {
|
||||
let Some(EntityType::Insert(insert)) = document.get_entity(top) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let base = crate::scene::render_graph::block_base_point(document, &insert.block_name);
|
||||
let mut chain = vec![ChainLevel {
|
||||
insert: top,
|
||||
block_name: insert.block_name.clone(),
|
||||
forward: crate::scene::render_graph::insert_transform_with_base(insert, base),
|
||||
}];
|
||||
let Some(point) = point else {
|
||||
return chain;
|
||||
};
|
||||
for _ in 0..16 {
|
||||
let level = chain.last().expect("chain starts with the top level").clone();
|
||||
let nearest = level_hits(document, &level)
|
||||
.into_iter()
|
||||
.filter_map(|(h, paths)| paths_distance(&paths, point).map(|d| (h, d)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let Some((handle, _)) = nearest else {
|
||||
break;
|
||||
};
|
||||
let Some(EntityType::Insert(inner)) = document.get_entity(handle) else {
|
||||
break;
|
||||
};
|
||||
if chain
|
||||
.iter()
|
||||
.any(|l| l.block_name.eq_ignore_ascii_case(&inner.block_name))
|
||||
{
|
||||
break;
|
||||
}
|
||||
let base = crate::scene::render_graph::block_base_point(document, &inner.block_name);
|
||||
let local = crate::scene::render_graph::insert_transform_with_base(inner, base);
|
||||
chain.push(ChainLevel {
|
||||
insert: handle,
|
||||
block_name: inner.block_name.clone(),
|
||||
forward: local.then(&level.forward),
|
||||
});
|
||||
}
|
||||
chain
|
||||
}
|
||||
|
||||
// ── REFEDIT / -REFEDIT pick command ───────────────────────────────────────
|
||||
|
||||
/// `Select reference:` — a block reference or xref of the current space.
|
||||
pub struct RefEditPickCommand {
|
||||
name: &'static str,
|
||||
references: HashSet<Handle>,
|
||||
}
|
||||
|
||||
impl RefEditPickCommand {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
/// `command_line` selects -REFEDIT (prompts) over REFEDIT.
|
||||
pub fn new(command_line: bool, references: HashSet<Handle>) -> Self {
|
||||
Self {
|
||||
name: if command_line { "-REFEDIT" } else { "REFEDIT" },
|
||||
references,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RefEditPickCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"REFEDIT"
|
||||
self.name
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
crate::t!("REFEDIT Select block reference to edit:").into_owned()
|
||||
t!("Select reference:").into_owned()
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
return CmdResult::ReportError(t!("Reference not found.").into_owned());
|
||||
}
|
||||
// Signal the host to enter the editing session for this handle.
|
||||
// We reuse Relaunch("REFEDIT_BEGIN:<handle>") as a convention.
|
||||
CmdResult::Relaunch(format!("REFEDIT_BEGIN:{}", handle.value()), vec![handle])
|
||||
if !self.references.contains(&handle) {
|
||||
return CmdResult::ReportError(t!("Object is not a valid reference").into_owned());
|
||||
}
|
||||
CmdResult::Relaunch(
|
||||
format!(
|
||||
"REFEDIT_PICKED:{}:{}:{}:{}:{}",
|
||||
(self.name == "-REFEDIT") as u8,
|
||||
handle.value(),
|
||||
pt.x,
|
||||
pt.y,
|
||||
pt.z
|
||||
),
|
||||
vec![handle],
|
||||
)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::ReportError(t!("Reference not found.").into_owned())
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::ReportError(t!("Reference not found.").into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
// ── -REFEDIT options after the pick ───────────────────────────────────────
|
||||
|
||||
enum OptionStep {
|
||||
Nesting,
|
||||
Method,
|
||||
Nested,
|
||||
Attributes,
|
||||
}
|
||||
|
||||
/// `Select nesting level [Ok/Next] <Next>:` → `Enter object selection method
|
||||
/// [All/Nested] <All>:` (→ `Select nested objects:`) → `Display attribute
|
||||
/// definitions [Yes/No] <No>:`, then the session starts.
|
||||
pub struct RefEditOptionsCommand {
|
||||
top: Handle,
|
||||
chain: Vec<ChainLevel>,
|
||||
hits: Vec<LevelHits>,
|
||||
level: usize,
|
||||
step: OptionStep,
|
||||
nested: Vec<Handle>,
|
||||
}
|
||||
|
||||
impl RefEditOptionsCommand {
|
||||
pub fn new(top: Handle, chain: Vec<ChainLevel>, hits: Vec<LevelHits>) -> Self {
|
||||
let level = chain.len().saturating_sub(1);
|
||||
Self {
|
||||
top,
|
||||
chain,
|
||||
hits,
|
||||
level,
|
||||
step: OptionStep::Nesting,
|
||||
nested: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn begin(&self, attributes: bool) -> CmdResult {
|
||||
let path: Vec<String> = self.chain[1..=self.level]
|
||||
.iter()
|
||||
.map(|l| l.insert.value().to_string())
|
||||
.collect();
|
||||
let nested: Vec<String> = self.nested.iter().map(|h| h.value().to_string()).collect();
|
||||
CmdResult::Relaunch(
|
||||
format!(
|
||||
"REFEDIT_BEGIN:{}:{}:{}:{}",
|
||||
self.top.value(),
|
||||
path.join(","),
|
||||
attributes as u8,
|
||||
nested.join(",")
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
}
|
||||
|
||||
fn pick_nested(&mut self, point: DVec3) -> CmdResult {
|
||||
let nearest = self
|
||||
.hits
|
||||
.get(self.level)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|(h, paths)| paths_distance(paths, point).map(|d| (*h, d)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1));
|
||||
match nearest {
|
||||
Some((h, _)) if !self.nested.contains(&h) => {
|
||||
self.nested.push(h);
|
||||
CmdResult::ReportMeasurement(t!("1 entity added").into_owned())
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RefEditOptionsCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"-REFEDIT"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
OptionStep::Nesting => t!("Select nesting level [Ok/Next] <Next>:").into_owned(),
|
||||
OptionStep::Method => {
|
||||
t!("Enter object selection method [All/Nested] <All>:").into_owned()
|
||||
}
|
||||
OptionStep::Nested => t!("Select nested objects:").into_owned(),
|
||||
OptionStep::Attributes => {
|
||||
t!("Display attribute definitions [Yes/No] <No>:").into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
OptionStep::Nesting => vec![CmdOption::new("Ok", "O"), CmdOption::new("Next", "N")],
|
||||
OptionStep::Method => vec![CmdOption::new("All", "A"), CmdOption::new("Nested", "N")],
|
||||
OptionStep::Nested => Vec::new(),
|
||||
OptionStep::Attributes => {
|
||||
vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")]
|
||||
}
|
||||
}
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(self.step, OptionStep::Nested)
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, OptionStep::Nested)
|
||||
}
|
||||
fn on_entity_pick(&mut self, _handle: Handle, pt: DVec3) -> CmdResult {
|
||||
self.pick_nested(pt)
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let word = text.trim().to_ascii_uppercase();
|
||||
if word.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let matches = |full: &str| full.starts_with(word.as_str());
|
||||
Some(match self.step {
|
||||
OptionStep::Nesting if matches("OK") => {
|
||||
self.step = OptionStep::Method;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
OptionStep::Nesting if matches("NEXT") => self.on_enter(),
|
||||
OptionStep::Method if matches("ALL") => self.on_enter(),
|
||||
OptionStep::Method if matches("NESTED") => {
|
||||
self.step = OptionStep::Nested;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
OptionStep::Attributes if matches("YES") => self.begin(true),
|
||||
OptionStep::Attributes if matches("NO") => self.begin(false),
|
||||
OptionStep::Nested => return None,
|
||||
_ => CmdResult::ReportError(t!("Invalid option keyword.").into_owned()),
|
||||
})
|
||||
}
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
OptionStep::Nested => self.pick_nested(pt),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
OptionStep::Nesting => {
|
||||
// Next steps outward and wraps back to the innermost level.
|
||||
self.level = if self.level == 0 {
|
||||
self.chain.len().saturating_sub(1)
|
||||
} else {
|
||||
self.level - 1
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
OptionStep::Method => {
|
||||
self.step = OptionStep::Attributes;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
OptionStep::Nested => {
|
||||
let n = self.nested.len();
|
||||
if n == 0 {
|
||||
return CmdResult::CancelWithMessage(t!("0 items selected").into_owned());
|
||||
}
|
||||
self.step = OptionStep::Attributes;
|
||||
CmdResult::ReportMeasurement(if n == 1 {
|
||||
t!("1 item selected").into_owned()
|
||||
} else {
|
||||
crate::tf!("{} items selected", n).into_owned()
|
||||
})
|
||||
}
|
||||
OptionStep::Attributes => self.begin(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── REFSET ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// `Enter an option [Add/Remove] <Add>:` then `Select objects:`.
|
||||
pub struct RefSetCommand {
|
||||
add: bool,
|
||||
gathering: bool,
|
||||
selected: Vec<Handle>,
|
||||
}
|
||||
|
||||
impl RefSetCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
add: true,
|
||||
gathering: false,
|
||||
selected: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for RefSetCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"REFSET"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
if self.gathering {
|
||||
t!("Select objects:").into_owned()
|
||||
} else {
|
||||
t!("Enter an option [Add/Remove] <Add>:").into_owned()
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.gathering {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![CmdOption::new("Add", "A"), CmdOption::new("Remove", "R")]
|
||||
}
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!self.gathering
|
||||
}
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.gathering
|
||||
}
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
self.selected = handles;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.gathering {
|
||||
return None;
|
||||
}
|
||||
let word = text.trim().to_ascii_uppercase();
|
||||
if word.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(if "ADD".starts_with(&word) {
|
||||
self.gathering = true;
|
||||
CmdResult::NeedPoint
|
||||
} else if "REMOVE".starts_with(&word) {
|
||||
self.add = false;
|
||||
self.gathering = true;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::ReportError(t!("Invalid option keyword.").into_owned())
|
||||
})
|
||||
}
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
if !self.gathering {
|
||||
self.gathering = true;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.selected.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
CmdResult::Relaunch(
|
||||
format!("REFSET_APPLY:{}", if self.add { "ADD" } else { "REMOVE" }),
|
||||
std::mem::take(&mut self.selected),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── REFCLOSE command ───────────────────────────────────────────────────────
|
||||
|
||||
/// Step 4: prompt for SAVE or DISCARD.
|
||||
/// `Enter option [Save/Discard reference changes] <Save>:`
|
||||
pub struct RefCloseCommand;
|
||||
|
||||
impl RefCloseCommand {
|
||||
@@ -120,25 +562,30 @@ impl CadCommand for RefCloseCommand {
|
||||
"REFCLOSE"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
t!("REFCLOSE [Save/Discard] <Save>:").into_owned()
|
||||
t!("Enter option [Save/Discard reference changes] <Save>:").into_owned()
|
||||
}
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
vec![CmdOption::new("Save", "S"), CmdOption::new("Discard", "D")]
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim().to_uppercase();
|
||||
let save = t.is_empty() || t == "S" || t == "SAVE";
|
||||
let discard = t == "D" || t == "DISCARD";
|
||||
if save {
|
||||
if t.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if "SAVE".starts_with(&t) {
|
||||
Some(CmdResult::Relaunch("REFCLOSE_SAVE".into(), vec![]))
|
||||
} else if discard {
|
||||
} else if "DISCARD".starts_with(&t) {
|
||||
Some(CmdResult::Relaunch("REFCLOSE_DISCARD".into(), vec![]))
|
||||
} else {
|
||||
Some(CmdResult::NeedPoint) // re-prompt
|
||||
Some(CmdResult::ReportError(
|
||||
t!("Invalid option keyword.").into_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Default: SAVE
|
||||
CmdResult::Relaunch("REFCLOSE_SAVE".into(), vec![])
|
||||
}
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
@@ -148,23 +595,48 @@ impl CadCommand for RefCloseCommand {
|
||||
|
||||
// ── Geometry helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/// Apply the INSERT's forward transform to a block-local entity so it
|
||||
/// appears at its correct world-space position. The full affine handles
|
||||
/// OCS, rotation and non-uniform / mirrored scale at once.
|
||||
/// Inverse of an affine placement (rotation / scale / mirror and translation),
|
||||
/// or None when it is singular.
|
||||
pub fn inverse_placement(t: &codec::types::Transform) -> Option<codec::types::Transform> {
|
||||
use codec::types::{Matrix3, Matrix4};
|
||||
let m = &t.matrix.m;
|
||||
let linear = Matrix3 {
|
||||
m: [
|
||||
[m[0][0], m[0][1], m[0][2]],
|
||||
[m[1][0], m[1][1], m[1][2]],
|
||||
[m[2][0], m[2][1], m[2][2]],
|
||||
],
|
||||
}
|
||||
.inverse()?;
|
||||
let r = linear.m;
|
||||
let tr = [m[0][3], m[1][3], m[2][3]];
|
||||
let mut out = Matrix4::identity();
|
||||
for row in 0..3 {
|
||||
for col in 0..3 {
|
||||
out.m[row][col] = r[row][col];
|
||||
}
|
||||
out.m[row][3] = -(r[row][0] * tr[0] + r[row][1] * tr[1] + r[row][2] * tr[2]);
|
||||
}
|
||||
Some(codec::types::Transform::from_matrix(out))
|
||||
}
|
||||
|
||||
|
||||
/// Apply the session's forward transform to a definition-local entity so it
|
||||
/// appears at its correct world-space position.
|
||||
pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) {
|
||||
entity.as_entity_mut().apply_transform(&session.forward);
|
||||
}
|
||||
|
||||
/// Apply the INSERT's inverse transform to a world-space entity to bring it
|
||||
/// back to block-local coordinates.
|
||||
/// Apply the session's inverse transform to a world-space entity to bring it
|
||||
/// back to definition-local coordinates.
|
||||
pub fn apply_insert_inverse_transform(entity: &mut EntityType, session: &RefEditSession) {
|
||||
entity.as_entity_mut().apply_transform(&session.inverse);
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["REFCLOSE"]
|
||||
}); // RefCloseCommand
|
||||
names: &["REFCLOSE", "REFSET", "-REFEDIT"]
|
||||
});
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["REFEDIT"]
|
||||
}); // RefEditPickCommand
|
||||
Vendored
+23
@@ -1531,6 +1531,29 @@ pub(crate) fn fade_toward_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
|
||||
]
|
||||
}
|
||||
|
||||
/// XFADECTL: how far objects outside the REFEDIT working set fade toward the
|
||||
/// background, in percent (0–90, default 70).
|
||||
static REFEDIT_FADE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(70);
|
||||
|
||||
pub fn refedit_fade_ctl() -> i32 {
|
||||
REFEDIT_FADE.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_refedit_fade_ctl(value: i32) {
|
||||
REFEDIT_FADE.store(value.clamp(0, 90), std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// A colour outside the REFEDIT working set, faded by XFADECTL.
|
||||
pub(crate) fn refedit_fade(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
|
||||
let t = refedit_fade_ctl().clamp(0, 90) as f32 / 100.0;
|
||||
[
|
||||
color[0] * (1.0 - t) + bg[0] * t,
|
||||
color[1] * (1.0 - t) + bg[1] * t,
|
||||
color[2] * (1.0 - t) + bg[2] * t,
|
||||
color[3],
|
||||
]
|
||||
}
|
||||
|
||||
/// XDWGFADECTL: how far referenced drawings fade toward the background, in
|
||||
/// percent (0–90); zero or negative shows them unfaded.
|
||||
static XREF_FADE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(50);
|
||||
|
||||
+14
-4
@@ -2147,6 +2147,8 @@ pub struct Scene {
|
||||
/// edited geometry stands out while the surrounding drawing stays visible
|
||||
/// for context. `None` = not editing. (#136)
|
||||
pub refedit_keep: Option<HashSet<Handle>>,
|
||||
/// REFEDIT locks the objects outside the working set (`refedit_keep`).
|
||||
pub refedit_lock: bool,
|
||||
/// COUNT mode colours: counted references in COUNTCOLOR, overlapping
|
||||
/// duplicates in COUNTERRORCOLOR, everything else faded. `None` outside
|
||||
/// count mode.
|
||||
@@ -2775,6 +2777,7 @@ impl Scene {
|
||||
preview_hidden: HashSet::default(),
|
||||
command_preview_hidden: HashSet::default(),
|
||||
refedit_keep: None,
|
||||
refedit_lock: false,
|
||||
count_display: None,
|
||||
hover_highlight: None,
|
||||
constraint_hover_highlights: HashSet::default(),
|
||||
@@ -4337,7 +4340,7 @@ impl Scene {
|
||||
);
|
||||
if let Some(keep) = &self.refedit_keep {
|
||||
if !keep.contains(&h) {
|
||||
material.diffuse = crate::scene::cache::block_cache::fade_toward_bg(
|
||||
material.diffuse = crate::scene::cache::block_cache::refedit_fade(
|
||||
material.diffuse,
|
||||
bg,
|
||||
);
|
||||
@@ -4393,7 +4396,7 @@ impl Scene {
|
||||
.is_some_and(|keep| !keep.contains(&handle))
|
||||
{
|
||||
material.diffuse =
|
||||
crate::scene::cache::block_cache::fade_toward_bg(material.diffuse, bg);
|
||||
crate::scene::cache::block_cache::refedit_fade(material.diffuse, bg);
|
||||
}
|
||||
if self.meshes.contains_key(&handle) {
|
||||
material.diffuse = self.count_color(handle, material.diffuse, bg);
|
||||
@@ -4538,7 +4541,7 @@ impl Scene {
|
||||
let keep_bright =
|
||||
Self::handle_from_wire_name(&w.name).is_some_and(|h| keep.contains(&h));
|
||||
if !keep_bright {
|
||||
w.color = crate::scene::cache::block_cache::fade_toward_bg(w.color, bg);
|
||||
w.color = crate::scene::cache::block_cache::refedit_fade(w.color, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9389,6 +9392,13 @@ impl Scene {
|
||||
.layers
|
||||
.get(&entity.common().layer)
|
||||
.is_some_and(|l| l.is_locked())
|
||||
// REFEDIT with "Lock objects not in working set": only the
|
||||
// working set can be edited.
|
||||
|| (self.refedit_lock
|
||||
&& self
|
||||
.refedit_keep
|
||||
.as_ref()
|
||||
.is_some_and(|keep| !keep.contains(&handle)))
|
||||
}
|
||||
|
||||
/// The name of the locked layer `handle` sits on, if any (for messages).
|
||||
@@ -9682,7 +9692,7 @@ impl Scene {
|
||||
};
|
||||
if let Some(keep) = &self.refedit_keep {
|
||||
if !keep.contains(&h) {
|
||||
c = crate::scene::cache::block_cache::fade_toward_bg(c, self.current_bg());
|
||||
c = crate::scene::cache::block_cache::refedit_fade(c, self.current_bg());
|
||||
}
|
||||
}
|
||||
Some(c)
|
||||
|
||||
Reference in new issue
Block a user