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:
Hakan Seven committed 2026-10-14 08:46:42 +03:00
commit 7e763080cb
16 files changed
+1296 -380

No files matched your search

+32
View File
@@ -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
+32
View File
@@ -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
+1 -1
View File
@@ -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;
+31
View File
@@ -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");
-326
View File
@@ -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;
+4
View File
@@ -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);
}
+588
View File
@@ -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);
}
}
_ => {}
}
}
}
+6
View File
@@ -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,
+4
View File
@@ -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
+8
View File
@@ -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,
+2
View File
@@ -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
View File
@@ -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);
+30
View File
@@ -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")),
+519 -47
View File
@@ -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
+23
View File
@@ -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
View File
@@ -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)