forked from 1b-agent/opencadstudio-upstream
fix(blocks): one base point convention for creation, redefine, insert, explode, XCLIP and REFEDIT
world = insert_point + Rot * Scale * (P - base_point). Attribute placement, EXPLODE, XCLIP storage, REFEDIT's working copy, the UCS reframe of attributes and the dynamic-block visibility grip ignored the base point and were offset by Rot*Scale*base. They now share insert_transform_with_base. Creation stores a ZERO base and redefine resets it; a no-op redefine moves nothing; the Block Definition dialog prefills the base from the definition. XCLIP filters stored by older builds keep their old displacement.
This commit is contained in:
1 parent
a456367b26
commit
5dfa41c032
17 files changed
+1447
-19
No files matched your search
@@ -1049,6 +1049,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Attributes are placed through the same base-point convention as
|
||||||
|
// the render path, so hand the command the definition's base point.
|
||||||
|
let block_base = crate::scene::render_graph::block_base_point(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&block_name,
|
||||||
|
);
|
||||||
|
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||||
|
cmd.attreq_set_block_base(block_base);
|
||||||
|
}
|
||||||
let completed = self.tabs[i]
|
let completed = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
|||||||
@@ -1479,3 +1479,278 @@ mod bedit_characterization_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod block_base_point_tests {
|
||||||
|
use super::super::*;
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use crate::modules::insert::insert_block::InsertBlockCommand;
|
||||||
|
use codec::entities::AttributeDefinition;
|
||||||
|
use codec::types::Vector3;
|
||||||
|
use codec::EntityType;
|
||||||
|
|
||||||
|
/// base-point fix (A), host side: `AttreqNeeded` is where the host resolves the
|
||||||
|
/// block's ATTDEFs, so it also injects the definition's base point. Without
|
||||||
|
/// that the committed attribute lands on the codec transform's position.
|
||||||
|
#[test]
|
||||||
|
fn locked_attreq_injects_the_block_base_point_for_attribute_placement() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
// Block "ATTREQ_BP": base (10,5,0); LINE (10,5)-(30,5); ATTDEF TAG1 at
|
||||||
|
// (12,8). Rendering convention (see scene::block_base_point_tests): the
|
||||||
|
// ATTDEF lands at (100,100) + Rot(30)*Scale(2)*((12,8) - (10,5))
|
||||||
|
// = (100,100) + 2*(1.732051 - 1.5, 1 + 2.598076) = (100.464102, 107.196152).
|
||||||
|
app.tabs[i].scene.define_block_raw(
|
||||||
|
"ATTREQ_BP",
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
vec![
|
||||||
|
EntityType::Line(codec::entities::Line::from_points(
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
Vector3::new(30.0, 5.0, 0.0),
|
||||||
|
)),
|
||||||
|
EntityType::AttributeDefinition(AttributeDefinition {
|
||||||
|
insertion_point: Vector3::new(12.0, 8.0, 0.0),
|
||||||
|
height: 1.0,
|
||||||
|
..AttributeDefinition::new(
|
||||||
|
"TAG1".to_string(),
|
||||||
|
"Tag one".to_string(),
|
||||||
|
String::new(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = InsertBlockCommand::classic_for_block(
|
||||||
|
"ATTREQ_BP".to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
String::new(),
|
||||||
|
);
|
||||||
|
let _ = cmd.on_point(glam::DVec3::new(100.0, 100.0, 0.0));
|
||||||
|
assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::NeedPoint)));
|
||||||
|
assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::NeedPoint)));
|
||||||
|
assert!(matches!(
|
||||||
|
cmd.on_text_input("30"),
|
||||||
|
Some(CmdResult::AttreqNeeded { .. })
|
||||||
|
));
|
||||||
|
app.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
|
|
||||||
|
let _ = app.apply_cmd_result(CmdResult::AttreqNeeded {
|
||||||
|
block_name: "ATTREQ_BP".to_string(),
|
||||||
|
});
|
||||||
|
let result = app.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.expect("the INSERT command waits for the attribute value")
|
||||||
|
.on_text_input("VALUE1")
|
||||||
|
.expect("the value finishes the INSERT");
|
||||||
|
let _ = app.apply_cmd_result(result);
|
||||||
|
|
||||||
|
let attribute = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.find_map(|entity| match entity {
|
||||||
|
EntityType::Insert(ins) if ins.block_name == "ATTREQ_BP" => {
|
||||||
|
ins.attributes.first().cloned()
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("the committed INSERT's attribute");
|
||||||
|
assert_eq!(attribute.tag, "TAG1");
|
||||||
|
let point = attribute.insertion_point;
|
||||||
|
assert!(
|
||||||
|
(point.x - 100.464102).abs() < 1e-6
|
||||||
|
&& (point.y - 107.196152).abs() < 1e-6
|
||||||
|
&& point.z.abs() < 1e-9,
|
||||||
|
"attribute at ({:.6},{:.6},{:.6}), expected (100.464102,107.196152,0)",
|
||||||
|
point.x,
|
||||||
|
point.y,
|
||||||
|
point.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// base-point fix (D), host side: picking an existing block in the Block
|
||||||
|
/// Definition dialog prefills the base point from that definition.
|
||||||
|
#[test]
|
||||||
|
fn locked_block_def_name_select_prefills_the_definition_base_point() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].scene.define_block_raw(
|
||||||
|
"DEF_BASE",
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
vec![EntityType::Line(codec::entities::Line::from_points(
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
Vector3::new(30.0, 5.0, 0.0),
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
app.block_definition = Some(
|
||||||
|
crate::ui::window::block_definition::BlockDefinitionState::new(
|
||||||
|
vec!["DEF_BASE".to_string()],
|
||||||
|
Vec::new(),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.block_definition
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.base_point_y.as_str()),
|
||||||
|
Some("0.0000"),
|
||||||
|
"a new block still starts at 0.0000"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = app.update(Message::BlockDefNameSelect("DEF_BASE".to_string()));
|
||||||
|
|
||||||
|
let state = app.block_definition.as_ref().expect("the dialog is open");
|
||||||
|
assert_eq!(state.name, "DEF_BASE");
|
||||||
|
assert_eq!(state.base_point_x, "10.0000");
|
||||||
|
assert_eq!(state.base_point_y, "5.0000");
|
||||||
|
assert_eq!(state.base_point_z, "0.0000");
|
||||||
|
assert_eq!(state.parse_base_point(), glam::DVec3::new(10.0, 5.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block with base (10,5,0) and LINE (10,5)-(30,5), referenced by an
|
||||||
|
/// INSERT at (100,100) with 30° rotation and uniform scale 2. Under the
|
||||||
|
/// convention the LINE renders at (100,100)-(134.641016,120).
|
||||||
|
fn block_and_instance_with_base(app: &mut OpenCADStudio, name: &str) -> codec::Handle {
|
||||||
|
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].scene.define_block_raw(
|
||||||
|
name,
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
vec![EntityType::Line(codec::entities::Line::from_points(
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
Vector3::new(30.0, 5.0, 0.0),
|
||||||
|
))],
|
||||||
|
);
|
||||||
|
let mut insert =
|
||||||
|
codec::entities::Insert::new(name.to_string(), Vector3::new(100.0, 100.0, 0.0));
|
||||||
|
insert.set_x_scale(2.0);
|
||||||
|
insert.set_y_scale(2.0);
|
||||||
|
insert.set_z_scale(2.0);
|
||||||
|
insert.rotation = std::f64::consts::PI / 6.0;
|
||||||
|
app.tabs[i].scene.add_entity(EntityType::Insert(insert))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// base-point follow-up fix: the REFEDIT working copy is placed through the
|
||||||
|
/// base-point-aware INSERT transform, so it sits exactly on the rendered
|
||||||
|
/// reference, and saving folds it back to the same block-local geometry.
|
||||||
|
#[test]
|
||||||
|
fn locked_refedit_working_copy_sits_on_the_rendered_reference() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let handle = block_and_instance_with_base(&mut app, "REFEDIT_BP");
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
let _ = app.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value()));
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].refedit_session.is_some(),
|
||||||
|
"setup: REFEDIT_BEGIN opens a session"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The working copy is the LINE the session moved into model space.
|
||||||
|
let temp: Vec<codec::Handle> = app.tabs[i]
|
||||||
|
.refedit_session
|
||||||
|
.as_ref()
|
||||||
|
.expect("the session")
|
||||||
|
.temp_handles
|
||||||
|
.clone();
|
||||||
|
let line = temp
|
||||||
|
.iter()
|
||||||
|
.find_map(|h| match app.tabs[i].scene.document.get_entity(*h) {
|
||||||
|
Some(EntityType::Line(line)) => Some(line.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("the working-copy LINE");
|
||||||
|
// (100,100) + Rot(30)*2*((10,5)-(10,5)) and ...*((30,5)-(10,5)).
|
||||||
|
assert!(
|
||||||
|
(line.start.x - 100.0).abs() < 1e-6
|
||||||
|
&& (line.start.y - 100.0).abs() < 1e-9
|
||||||
|
&& (line.end.x - 134.641016).abs() < 1e-6
|
||||||
|
&& (line.end.y - 120.0).abs() < 1e-6,
|
||||||
|
"working copy at ({:.6},{:.6})-({:.6},{:.6}), expected (100,100)-(134.641016,120)",
|
||||||
|
line.start.x,
|
||||||
|
line.start.y,
|
||||||
|
line.end.x,
|
||||||
|
line.end.y
|
||||||
|
);
|
||||||
|
|
||||||
|
// Saving without editing restores the definition verbatim: the
|
||||||
|
// inverse is the exact inverse of the forward transform.
|
||||||
|
let _ = app.dispatch_command("REFCLOSE_SAVE");
|
||||||
|
assert!(app.tabs[i].refedit_session.is_none());
|
||||||
|
let stored = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get("REFEDIT_BP")
|
||||||
|
.expect("the block record")
|
||||||
|
.entity_handles
|
||||||
|
.iter()
|
||||||
|
.find_map(|h| match app.tabs[i].scene.document.get_entity(*h) {
|
||||||
|
Some(EntityType::Line(line)) => Some(line.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("the stored LINE");
|
||||||
|
assert!(
|
||||||
|
(stored.start.x - 10.0).abs() < 1e-9
|
||||||
|
&& (stored.start.y - 5.0).abs() < 1e-9
|
||||||
|
&& (stored.end.x - 30.0).abs() < 1e-9
|
||||||
|
&& (stored.end.y - 5.0).abs() < 1e-9,
|
||||||
|
"stored LINE at ({:.9},{:.9})-({:.9},{:.9}), expected (10,5)-(30,5)",
|
||||||
|
stored.start.x,
|
||||||
|
stored.start.y,
|
||||||
|
stored.end.x,
|
||||||
|
stored.end.y
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
crate::scene::render_graph::block_base_point(&app.tabs[i].scene.document, "REFEDIT_BP"),
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
"the base point survives the round trip"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// base-point follow-up fix: the dynamic-block visibility grip is a
|
||||||
|
/// block-definition coordinate, so it follows the base-point convention
|
||||||
|
/// too: (100,100) + Rot(30)*2*((12,3) - (10,5)) = (105.464102, 98.535898).
|
||||||
|
#[test]
|
||||||
|
fn locked_visibility_grip_follows_the_base_point_convention() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let handle = block_and_instance_with_base(&mut app, "VIS_BP");
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
let def_block = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get("VIS_BP")
|
||||||
|
.expect("the block record")
|
||||||
|
.handle;
|
||||||
|
let mut param = codec::objects::BlockVisibilityParameter::default();
|
||||||
|
param.owner = def_block;
|
||||||
|
param.def_point = Vector3::new(12.0, 3.0, 0.0);
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.block_visibility_params
|
||||||
|
.insert(codec::Handle::new(0x70), param);
|
||||||
|
app.tabs[i].selected_handle = Some(handle);
|
||||||
|
|
||||||
|
app.refresh_visibility_grip([0.0, 0.0, 0.0]);
|
||||||
|
|
||||||
|
let grip = app.tabs[i]
|
||||||
|
.selected_grips
|
||||||
|
.last()
|
||||||
|
.expect("the visibility lookup grip");
|
||||||
|
assert!(
|
||||||
|
(grip.world.x - 105.464102).abs() < 1e-6 && (grip.world.y - 98.535898).abs() < 1e-6,
|
||||||
|
"grip at ({:.6},{:.6}), expected (105.464102,98.535898)",
|
||||||
|
grip.world.x,
|
||||||
|
grip.world.y
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].visibility_grip.is_some(),
|
||||||
|
"the selection is a dynamic-block reference"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -809,12 +809,19 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build the INSERT's full placement transform (OCS + rotation +
|
// Build the INSERT's full placement transform (OCS + rotation +
|
||||||
// scale, including non-uniform / mirrored) and its inverse, so
|
// scale, including non-uniform / mirrored, minus the block base
|
||||||
// edits round-trip back to block-local coordinates on SAVE.
|
// 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 sx = insert.x_scale();
|
||||||
let sy = insert.y_scale();
|
let sy = insert.y_scale();
|
||||||
let sz = insert.z_scale();
|
let sz = insert.z_scale();
|
||||||
let forward = insert.get_transform();
|
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 = {
|
let inverse = {
|
||||||
use codec::types::{Matrix3, Matrix4, Transform};
|
use codec::types::{Matrix3, Matrix4, Transform};
|
||||||
let ocs_t =
|
let ocs_t =
|
||||||
@@ -826,8 +833,11 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
let r_inv = Matrix4::rotation_z(-insert.rotation);
|
let r_inv = Matrix4::rotation_z(-insert.rotation);
|
||||||
let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz);
|
let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz);
|
||||||
// inverse(OCS·T·R·S) = S⁻¹·R⁻¹·T⁻¹·OCSᵀ
|
// inverse(T(-base)·OCS·T·R·S) = T(+base)·S⁻¹·R⁻¹·T⁻¹·OCSᵀ
|
||||||
Transform::from_matrix(s_inv * r_inv * t_inv * ocs_t)
|
Transform::from_matrix(
|
||||||
|
Matrix4::translation(base.x, base.y, base.z)
|
||||||
|
* (s_inv * r_inv * t_inv * ocs_t),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the block record.
|
// Find the block record.
|
||||||
|
|||||||
@@ -2269,8 +2269,8 @@ impl OpenCADStudio {
|
|||||||
let host = self.tabs[i].scene.document.header.insertion_units;
|
let host = self.tabs[i].scene.document.header.insertion_units;
|
||||||
let k = crate::app::properties::insert_unit_scale(host, units).unwrap_or(1.0);
|
let k = crate::app::properties::insert_unit_scale(host, units).unwrap_or(1.0);
|
||||||
let mut insert = codec::entities::Insert::new(name.to_string(), at).with_scale(k, k, k);
|
let mut insert = codec::entities::Insert::new(name.to_string(), at).with_scale(k, k, k);
|
||||||
let transform = insert.get_transform();
|
|
||||||
let doc = &self.tabs[i].scene.document;
|
let doc = &self.tabs[i].scene.document;
|
||||||
|
let transform = crate::scene::render_graph::insert_transform(doc, &insert);
|
||||||
let attdefs: Vec<codec::entities::AttributeDefinition> = doc
|
let attdefs: Vec<codec::entities::AttributeDefinition> = doc
|
||||||
.block_records
|
.block_records
|
||||||
.get(name)
|
.get(name)
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ use crate::scene::pick::xclip as clip;
|
|||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// A block reference's extents in WCS: its block's objects through the
|
/// A block reference's extents in WCS: its block's objects through the
|
||||||
/// insert transform.
|
/// insert transform — the base-point-aware one, because that is where the
|
||||||
|
/// renderer puts the contents the user drew the boundary around.
|
||||||
fn insert_extents(&self, i: usize, insert: codec::Handle) -> ([f64; 2], [f64; 2]) {
|
fn insert_extents(&self, i: usize, insert: codec::Handle) -> ([f64; 2], [f64; 2]) {
|
||||||
let document = &self.tabs[i].scene.document;
|
let document = &self.tabs[i].scene.document;
|
||||||
let Some(codec::EntityType::Insert(ins)) = document.get_entity(insert) else {
|
let Some(codec::EntityType::Insert(ins)) = document.get_entity(insert) else {
|
||||||
return ([0.0; 2], [0.0; 2]);
|
return ([0.0; 2], [0.0; 2]);
|
||||||
};
|
};
|
||||||
let xform = ins.get_transform();
|
let xform = crate::scene::render_graph::insert_transform(document, ins);
|
||||||
let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]);
|
let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]);
|
||||||
for e in document.entities_in_block(&ins.block_name) {
|
for e in document.entities_in_block(&ins.block_name) {
|
||||||
let (a, b) = crate::scene::convert::tess::entity_bounds_in(document, e);
|
let (a, b) = crate::scene::convert::tess::entity_bounds_in(document, e);
|
||||||
@@ -77,7 +78,14 @@ impl OpenCADStudio {
|
|||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let outline = clip::clip_outline_world(doc, filter, &ins.get_transform());
|
// The same base-point-aware transform the spatial filter
|
||||||
|
// was stored with, so the generated polyline lands on the
|
||||||
|
// boundary the user drew (the two cancel exactly).
|
||||||
|
let outline = clip::clip_outline_world(
|
||||||
|
doc,
|
||||||
|
filter,
|
||||||
|
&crate::scene::render_graph::insert_transform(doc, ins),
|
||||||
|
);
|
||||||
let mut pl = codec::entities::LwPolyline::from_points(
|
let mut pl = codec::entities::LwPolyline::from_points(
|
||||||
outline
|
outline
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -5017,9 +5017,16 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Message::BlockDefNameSelect(chosen) => {
|
Message::BlockDefNameSelect(chosen) => {
|
||||||
if let Some(state) = self.block_definition.as_mut() {
|
if let Some(state) = self.block_definition.as_mut() {
|
||||||
state.name = chosen;
|
state.name = chosen.clone();
|
||||||
state.error_message = None;
|
state.error_message = None;
|
||||||
state.confirm_redefine = None;
|
state.confirm_redefine = None;
|
||||||
|
// Redefining an existing block starts from the base point
|
||||||
|
// that definition already has (0.0000 for a new block).
|
||||||
|
let base = crate::scene::render_graph::block_base_point(
|
||||||
|
&self.tabs[self.active_tab].scene.document,
|
||||||
|
&chosen,
|
||||||
|
);
|
||||||
|
state.set_base_point(glam::DVec3::new(base.x, base.y, base.z));
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,10 @@ impl OpenCADStudio {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let wp = ins.get_transform().apply(Vector3::new(
|
// `def_point` is a block-definition coordinate (see the codec's
|
||||||
|
// BlockVisibilityParameter), so the grip follows the same base-point
|
||||||
|
// convention as the rendered reference.
|
||||||
|
let wp = crate::scene::render_graph::insert_transform(doc, ins).apply(Vector3::new(
|
||||||
param.def_point.x,
|
param.def_point.x,
|
||||||
param.def_point.y,
|
param.def_point.y,
|
||||||
param.def_point.z,
|
param.def_point.z,
|
||||||
|
|||||||
@@ -3035,6 +3035,11 @@ pub trait CadCommand: Send {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Supply the referenced block's insertion base point so the attributes
|
||||||
|
/// the command now owns are placed through the same base-point convention
|
||||||
|
/// as the render path. Default: no-op (no base point).
|
||||||
|
fn attreq_set_block_base(&mut self, _base: codec::types::Vector3) {}
|
||||||
|
|
||||||
/// After the host commits an ATTREQ insert: keep the command running
|
/// After the host commits an ATTREQ insert: keep the command running
|
||||||
/// (repeated placement) instead of ending it.
|
/// (repeated placement) instead of ending it.
|
||||||
fn attreq_continue(&self) -> bool {
|
fn attreq_continue(&self) -> bool {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ use codec::entities::{
|
|||||||
};
|
};
|
||||||
use codec::entities::{Polyline, Polyline2D};
|
use codec::entities::{Polyline, Polyline2D};
|
||||||
use codec::tables::BlockRecord;
|
use codec::tables::BlockRecord;
|
||||||
use codec::types::Vector3;
|
use codec::types::{Transform, Vector3};
|
||||||
use codec::{CadDocument, EntityType, Handle};
|
use codec::{CadDocument, Entity, EntityType, Handle};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||||
use crate::entities::curve::lwpolyline_world_xy;
|
use crate::entities::curve::lwpolyline_world_xy;
|
||||||
@@ -106,7 +106,24 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
|
|||||||
if !explodable {
|
if !explodable {
|
||||||
vec![]
|
vec![]
|
||||||
} else {
|
} else {
|
||||||
ins.explode_from_document(document)
|
// The codec applies only `Insert::get_transform`, which does
|
||||||
|
// not know the block's base point, so shift the definition's
|
||||||
|
// contents by -base first: get_transform(P - base) is exactly
|
||||||
|
// world = insert_point + Rot*Scale*(P - base), the placement
|
||||||
|
// the render path uses. MINSERT cells and nested references
|
||||||
|
// get the same treatment because the shift happens on the
|
||||||
|
// contents, not on the insert.
|
||||||
|
let base = crate::scene::render_graph::block_base_point(document, &ins.block_name);
|
||||||
|
let to_block = Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z));
|
||||||
|
let entities: Vec<EntityType> = document
|
||||||
|
.entities_in_block(&ins.block_name)
|
||||||
|
.map(|entity| {
|
||||||
|
let mut shifted = entity.clone();
|
||||||
|
Entity::apply_transform(shifted.as_entity_mut(), &to_block);
|
||||||
|
shifted
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
ins.explode(&entities)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(normalize_insert_entity)
|
.map(normalize_insert_entity)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ pub struct InsertBlockCommand {
|
|||||||
rotation: f64,
|
rotation: f64,
|
||||||
pending_insert: Option<Insert>,
|
pending_insert: Option<Insert>,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
|
/// The referenced block's insertion base point. Attributes are placed
|
||||||
|
/// through the same base-point convention the render path uses, so the
|
||||||
|
/// host injects it when it resolves the block's ATTDEFs.
|
||||||
|
block_base: Vector3,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InsertBlockCommand {
|
impl InsertBlockCommand {
|
||||||
@@ -152,6 +156,7 @@ impl InsertBlockCommand {
|
|||||||
rotation: 0.0,
|
rotation: 0.0,
|
||||||
pending_insert: None,
|
pending_insert: None,
|
||||||
plane: WorkingPlane::default(),
|
plane: WorkingPlane::default(),
|
||||||
|
block_base: Vector3::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -852,9 +857,18 @@ impl CadCommand for InsertBlockCommand {
|
|||||||
fn attreq_take_insert(&mut self) -> Option<codec::EntityType> {
|
fn attreq_take_insert(&mut self) -> Option<codec::EntityType> {
|
||||||
self.pending_insert.take().map(EntityType::Insert)
|
self.pending_insert.take().map(EntityType::Insert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn attreq_set_block_base(&mut self, base: Vector3) {
|
||||||
|
self.set_block_base(base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InsertBlockCommand {
|
impl InsertBlockCommand {
|
||||||
|
/// The referenced block's insertion base point, for attribute placement.
|
||||||
|
pub fn set_block_base(&mut self, base: Vector3) {
|
||||||
|
self.block_base = base;
|
||||||
|
}
|
||||||
|
|
||||||
fn answer(&mut self, ask: Ask, text: Option<&str>) -> CmdResult {
|
fn answer(&mut self, ask: Ask, text: Option<&str>) -> CmdResult {
|
||||||
let default = !matches!(ask, Ask::RepeatScale | Ask::RepeatRotate);
|
let default = !matches!(ask, Ask::RepeatScale | Ask::RepeatRotate);
|
||||||
let Some(yes) = Self::yes_no(text.unwrap_or(""), default) else {
|
let Some(yes) = Self::yes_no(text.unwrap_or(""), default) else {
|
||||||
@@ -955,7 +969,8 @@ impl InsertBlockCommand {
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let mut insert = self.pending_insert.take()?;
|
let mut insert = self.pending_insert.take()?;
|
||||||
let transform = insert.get_transform();
|
let transform =
|
||||||
|
crate::scene::render_graph::insert_transform_with_base(&insert, self.block_base);
|
||||||
for (attdef_idx, value) in values {
|
for (attdef_idx, value) in values {
|
||||||
let Some(attdef) = attdefs.get(attdef_idx) else {
|
let Some(attdef) = attdefs.get(attdef_idx) else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -947,6 +947,14 @@ impl Scene {
|
|||||||
&mut self,
|
&mut self,
|
||||||
options: CreateBlockOptions,
|
options: CreateBlockOptions,
|
||||||
) -> Result<Handle, String> {
|
) -> Result<Handle, String> {
|
||||||
|
// Base-point contract: the contents are stored *relative to the picked
|
||||||
|
// base* because `world_to_block` already subtracts it, so the
|
||||||
|
// definition's own `Block.base_point` stays ZERO and the picked base
|
||||||
|
// becomes the block origin (inserting at a point puts that point where
|
||||||
|
// the base was). Callers must not also store the picked base in the
|
||||||
|
// definition — that would subtract it twice. Imported definitions,
|
||||||
|
// whose geometry arrives relative to their own base point, keep that
|
||||||
|
// base point and are never rewritten here.
|
||||||
let name = options.name.trim();
|
let name = options.name.trim();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Err("Block name cannot be empty.".into());
|
return Err("Block name cannot be empty.".into());
|
||||||
@@ -988,6 +996,11 @@ impl Scene {
|
|||||||
// Update existing Block entity fields
|
// Update existing Block entity fields
|
||||||
if let Some(EntityType::Block(b)) = self.document.get_entity_mut(existing_blk_h) {
|
if let Some(EntityType::Block(b)) = self.document.get_entity_mut(existing_blk_h) {
|
||||||
b.description = options.description.clone();
|
b.description = options.description.clone();
|
||||||
|
// The new contents are baked relative to the picked base (see
|
||||||
|
// the contract above), so the definition's base point has to
|
||||||
|
// go back to ZERO. Leaving an imported definition's non-zero
|
||||||
|
// base point here would subtract the picked base twice.
|
||||||
|
b.base_point = codec::types::Vector3::ZERO;
|
||||||
}
|
}
|
||||||
if options.annotative {
|
if options.annotative {
|
||||||
let annotative_vals = vec![codec::xdata::XDataValue::Integer16(1)];
|
let annotative_vals = vec![codec::xdata::XDataValue::Integer16(1)];
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ pub enum BlockScalePolicy {
|
|||||||
|
|
||||||
// Topic submodules split out of this root (each contributes `impl Scene`
|
// Topic submodules split out of this root (each contributes `impl Scene`
|
||||||
// blocks and/or free functions). Pure text-move from the original mod.rs.
|
// blocks and/or free functions). Pure text-move from the original mod.rs.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod block_base_point_tests;
|
||||||
mod boundary;
|
mod boundary;
|
||||||
mod camera_ops;
|
mod camera_ops;
|
||||||
pub(crate) mod centerline;
|
pub(crate) mod centerline;
|
||||||
|
|||||||
+7
-2
@@ -506,7 +506,11 @@ impl Scene {
|
|||||||
|
|
||||||
// ATTRIBs live inline on each INSERT in that reference's owner space.
|
// ATTRIBs live inline on each INSERT in that reference's owner space.
|
||||||
// Conjugate the block-local reframe through the INSERT transform:
|
// Conjugate the block-local reframe through the INSERT transform:
|
||||||
// owner' = M · local_from_old · M⁻¹ · owner.
|
// owner' = M · local_from_old · M⁻¹ · owner. M is the block-local →
|
||||||
|
// world map the render path uses (`insert_transform`), because that is
|
||||||
|
// the map the attribute positions on the INSERT were placed through;
|
||||||
|
// the codec's base-ignoring transform would conjugate them through a
|
||||||
|
// displaced frame whenever the definition has a non-zero base point.
|
||||||
let references: Vec<(Handle, codec::types::Transform)> = self
|
let references: Vec<(Handle, codec::types::Transform)> = self
|
||||||
.document
|
.document
|
||||||
.entities()
|
.entities()
|
||||||
@@ -516,7 +520,8 @@ impl Scene {
|
|||||||
&& !reference.attributes.is_empty()
|
&& !reference.attributes.is_empty()
|
||||||
&& !owned.contains(&reference.common.handle) =>
|
&& !owned.contains(&reference.common.handle) =>
|
||||||
{
|
{
|
||||||
let insertion = reference.get_transform();
|
let insertion =
|
||||||
|
crate::scene::render_graph::insert_transform(&self.document, reference);
|
||||||
let inverse = inverse_affine(&insertion)?;
|
let inverse = inverse_affine(&insertion)?;
|
||||||
Some((
|
Some((
|
||||||
reference.common.handle,
|
reference.common.handle,
|
||||||
|
|||||||
@@ -250,7 +250,12 @@ pub fn set_insert_clip(
|
|||||||
let Some(codec::EntityType::Insert(ins)) = doc.get_entity(insert) else {
|
let Some(codec::EntityType::Insert(ins)) = doc.get_entity(insert) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let t = ins.get_transform().matrix.m;
|
// The stored inverse is the inverse of the transform every consumer
|
||||||
|
// composes the boundary back with — the base-point-aware instance
|
||||||
|
// transform the render path uses. Storing the codec's base-ignoring
|
||||||
|
// inverse would displace the ring by exactly -base on the next render.
|
||||||
|
let forward = crate::scene::render_graph::insert_transform(doc, ins);
|
||||||
|
let t = forward.matrix.m;
|
||||||
let forward = glam::DMat4::from_cols_array_2d(&[
|
let forward = glam::DMat4::from_cols_array_2d(&[
|
||||||
[t[0][0], t[1][0], t[2][0], t[3][0]],
|
[t[0][0], t[1][0], t[2][0], t[3][0]],
|
||||||
[t[0][1], t[1][1], t[2][1], t[3][1]],
|
[t[0][1], t[1][1], t[2][1], t[3][1]],
|
||||||
|
|||||||
@@ -688,7 +688,14 @@ pub(crate) fn clear_dim_picture_placement(entity: &mut EntityType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_transform(document: &CadDocument, insert: &Insert) -> Transform {
|
pub fn insert_transform(document: &CadDocument, insert: &Insert) -> Transform {
|
||||||
let base = block_base_point(document, &insert.block_name);
|
insert_transform_with_base(insert, block_base_point(document, &insert.block_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`insert_transform`] for a caller that already resolved the block's base
|
||||||
|
/// point (the `-INSERT` command builds the INSERT before the drawing is in
|
||||||
|
/// scope), so the base-point convention lives in exactly one place:
|
||||||
|
/// world = insert_point + Rot(rotation) * Scale * (P - base).
|
||||||
|
pub fn insert_transform_with_base(insert: &Insert, base: Vector3) -> Transform {
|
||||||
Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z))
|
Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z))
|
||||||
.then(&insert.get_transform())
|
.then(&insert.get_transform())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,15 @@ impl BlockDefinitionState {
|
|||||||
glam::DVec3::new(x, y, z)
|
glam::DVec3::new(x, y, z)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefill the base point fields from an existing definition, so
|
||||||
|
/// redefining a block starts from the base it already has (0.0000 for a
|
||||||
|
/// new block). Formatted the same way the pick point button writes them.
|
||||||
|
pub fn set_base_point(&mut self, base: glam::DVec3) {
|
||||||
|
self.base_point_x = format!("{:.4}", base.x);
|
||||||
|
self.base_point_y = format!("{:.4}", base.y);
|
||||||
|
self.base_point_z = format!("{:.4}", base.z);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_existing_name(&self) -> bool {
|
pub fn is_existing_name(&self) -> bool {
|
||||||
let trimmed = self.name.trim();
|
let trimmed = self.name.trim();
|
||||||
!trimmed.is_empty()
|
!trimmed.is_empty()
|
||||||
|
|||||||
Reference in new issue
Block a user