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:
Karim Jerbi committed 2026-10-12 04:35:19 +01:00
1 parent a456367b26
commit 5dfa41c032
17 files changed
+1447 -19

No files matched your search

+20 -3
View File
@@ -23,8 +23,8 @@ use codec::entities::{
};
use codec::entities::{Polyline, Polyline2D};
use codec::tables::BlockRecord;
use codec::types::Vector3;
use codec::{CadDocument, EntityType, Handle};
use codec::types::{Transform, Vector3};
use codec::{CadDocument, Entity, EntityType, Handle};
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::entities::curve::lwpolyline_world_xy;
@@ -106,7 +106,24 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
if !explodable {
vec![]
} 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()
.map(normalize_insert_entity)
.collect()
+16 -1
View File
@@ -126,6 +126,10 @@ pub struct InsertBlockCommand {
rotation: f64,
pending_insert: Option<Insert>,
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 {
@@ -152,6 +156,7 @@ impl InsertBlockCommand {
rotation: 0.0,
pending_insert: None,
plane: WorkingPlane::default(),
block_base: Vector3::ZERO,
}
}
@@ -852,9 +857,18 @@ impl CadCommand for InsertBlockCommand {
fn attreq_take_insert(&mut self) -> Option<codec::EntityType> {
self.pending_insert.take().map(EntityType::Insert)
}
fn attreq_set_block_base(&mut self, base: Vector3) {
self.set_block_base(base);
}
}
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 {
let default = !matches!(ask, Ask::RepeatScale | Ask::RepeatRotate);
let Some(yes) = Self::yes_no(text.unwrap_or(""), default) else {
@@ -955,7 +969,8 @@ impl InsertBlockCommand {
_ => return None,
};
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 {
let Some(attdef) = attdefs.get(attdef_idx) else {
continue;