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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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

-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;