REFEDIT: reference prompts, -REFEDIT, REFSET, nested references, xref write-back

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-13 15:52:26 +03:00
1 parent b1d4c9bd9b
commit 6784f89993
16 files changed
+1296 -380

No files matched your search

+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() {
@@ -785,303 +756,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
@@ -227,6 +227,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
@@ -662,6 +666,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
@@ -1177,6 +1177,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
@@ -1211,6 +1212,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
@@ -1237,6 +1239,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| {
@@ -1268,6 +1271,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
@@ -638,6 +638,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,
@@ -708,6 +711,10 @@ fn default_block_view() -> u8 {
1
}
fn default_xfadectl() -> u8 {
70
}
fn default_block_mru_list() -> u8 {
50
}
@@ -910,6 +917,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
@@ -866,6 +866,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,
@@ -1019,6 +1020,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;