forked from 1b-agent/opencadstudio-upstream
REFEDIT: reference prompts, -REFEDIT, REFSET, nested references, xref write-back
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b1d4c9bd9b
commit
6784f89993
16 files changed
+1296
-380
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user