fix(paste): UTM-precise paste/block base via f64 centroid

entities_centroid summed the f32 points (double-single HIGH half) alone, so
at UTM scale (~5.7e6) it both quantized each term ~0.5 m and lost low bits
across the running total — drifting the paste base and PASTEBLOCK's block
base metres off, which placed the wrapped block at the wrong spot/scale.

It now reconstructs each vertex's absolute f64 from points + points_low and
accumulates in f64. clipboard_centroid is DVec3; the PASTECLIP commit delta
is fully f64; define_block_from_owned_entities takes an f64 base so the
block-local fold (Translate(-base)) stays precise. The interactive ghost
anchors (PasteCommand, InsertBlockCommand) still take the f32 cast — display
only; the committed geometry is f64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-24 10:42:58 +03:00
commit 60d4712179
5 changed files with 31 additions and 21 deletions

View file

@ -616,8 +616,7 @@ impl OpenCADStudio {
tgt_wo[1] - src_wo[1], tgt_wo[1] - src_wo[1],
tgt_wo[2] - src_wo[2], tgt_wo[2] - src_wo[2],
); );
let delta = let delta = base_pt - self.clipboard_centroid + wo_corr;
base_pt - self.clipboard_centroid.as_dvec3() + wo_corr;
let translate = crate::command::EntityTransform::Translate(delta); let translate = crate::command::EntityTransform::Translate(delta);
self.push_undo_snapshot(i, "PASTECLIP"); self.push_undo_snapshot(i, "PASTECLIP");
let count = self.clipboard.len(); let count = self.clipboard.len();

View file

@ -603,13 +603,15 @@ impl OpenCADStudio {
let src_wo = self.clipboard_world_offset; let src_wo = self.clipboard_world_offset;
let tgt_wo = [0.0_f64; 3]; let tgt_wo = [0.0_f64; 3];
let centroid = self.clipboard_centroid let centroid = self.clipboard_centroid
+ glam::Vec3::new( + glam::DVec3::new(
(src_wo[0] - tgt_wo[0]) as f32, src_wo[0] - tgt_wo[0],
(src_wo[1] - tgt_wo[1]) as f32, src_wo[1] - tgt_wo[1],
(src_wo[2] - tgt_wo[2]) as f32, src_wo[2] - tgt_wo[2],
); );
use crate::modules::draw::clipboard::paste::PasteCommand; use crate::modules::draw::clipboard::paste::PasteCommand;
let cmd = PasteCommand::new(wires, centroid); // The ghost anchor is a display-only offset; the precise
// paste delta is computed in f64 at commit time.
let cmd = PasteCommand::new(wires, centroid.as_vec3());
self.command_line.push_info(&cmd.prompt()); self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd)); self.tabs[i].active_cmd = Some(Box::new(cmd));
} }
@ -674,7 +676,7 @@ impl OpenCADStudio {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
use crate::modules::insert::insert_block::InsertBlockCommand; use crate::modules::insert::insert_block::InsertBlockCommand;
let cmd = InsertBlockCommand::new_for_block(name, wires, base); let cmd = InsertBlockCommand::new_for_block(name, wires, base.as_vec3());
self.command_line.push_info(&cmd.prompt()); self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd)); self.tabs[i].active_cmd = Some(Box::new(cmd));
} }

View file

@ -254,25 +254,34 @@ pub(super) fn polar_constrain_near(
// ── Clipboard / selection helpers ────────────────────────────────────────── // ── Clipboard / selection helpers ──────────────────────────────────────────
/// Compute the centroid of a set of wire models (average of all points). /// Compute the centroid of a set of wire models (average of all points).
pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::Vec3 { pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::DVec3 {
let mut sum = glam::Vec3::ZERO; // Reconstruct each vertex's absolute f64 from the double-single high/low
// pair and accumulate in f64: summing the f32 `points` alone at UTM scale
// (~5.7e6) both quantizes each term ~0.5 m and loses low bits across the
// running total, drifting the paste base / block base metres off.
let mut sum = glam::DVec3::ZERO;
let mut count = 0usize; let mut count = 0usize;
for w in wires { for w in wires {
for p in &w.points { for (i, p) in w.points.iter().enumerate() {
let v = glam::Vec3::from(*p);
// Wire models carry NaN points as separators between disjoint // Wire models carry NaN points as separators between disjoint
// segments; summing them poisons the whole centroid into NaN, // segments; summing them poisons the whole centroid into NaN,
// which then makes every paste base point NaN. (#129) // which then makes every paste base point NaN. (#129)
if v.is_finite() { if !p[0].is_finite() || !p[1].is_finite() || !p[2].is_finite() {
sum += v; continue;
}
let l = w.points_low.get(i).copied().unwrap_or([0.0; 3]);
sum += glam::DVec3::new(
p[0] as f64 + l[0] as f64,
p[1] as f64 + l[1] as f64,
p[2] as f64 + l[2] as f64,
);
count += 1; count += 1;
} }
} }
}
if count > 0 { if count > 0 {
sum / count as f32 sum / count as f64
} else { } else {
glam::Vec3::ZERO glam::DVec3::ZERO
} }
} }

View file

@ -371,7 +371,7 @@ pub(super) struct OpenCADStudio {
/// In-memory clipboard: cloned entities waiting to be pasted. /// In-memory clipboard: cloned entities waiting to be pasted.
clipboard: Vec<acadrust::EntityType>, clipboard: Vec<acadrust::EntityType>,
/// Centroid of the clipboard entities (world XY plane). /// Centroid of the clipboard entities (world XY plane).
clipboard_centroid: glam::Vec3, clipboard_centroid: glam::DVec3,
/// The source drawing's `world_offset` at copy time. The centroid above is /// The source drawing's `world_offset` at copy time. The centroid above is
/// measured in that drawing's offset-relative frame; pasting into a drawing /// measured in that drawing's offset-relative frame; pasting into a drawing
/// with a different offset (e.g. a large-coordinate file → a fresh one) /// with a different offset (e.g. a large-coordinate file → a fresh one)
@ -1808,7 +1808,7 @@ impl OpenCADStudio {
update_notice_version: None, update_notice_version: None,
update_notice_body: None, update_notice_body: None,
clipboard: Vec::new(), clipboard: Vec::new(),
clipboard_centroid: glam::Vec3::ZERO, clipboard_centroid: glam::DVec3::ZERO,
clipboard_world_offset: [0.0; 3], clipboard_world_offset: [0.0; 3],
clipboard_deps: ClipboardDeps::default(), clipboard_deps: ClipboardDeps::default(),
shift_down: false, shift_down: false,

View file

@ -4204,7 +4204,7 @@ impl Scene {
&mut self, &mut self,
entities: Vec<EntityType>, entities: Vec<EntityType>,
name: &str, name: &str,
base: glam::Vec3, base: glam::DVec3,
) -> Result<(), String> { ) -> Result<(), String> {
let name = name.trim(); let name = name.trim();
if name.is_empty() { if name.is_empty() {
@ -4248,7 +4248,7 @@ impl Scene {
.add_entity(EntityType::BlockEnd(block_end)) .add_entity(EntityType::BlockEnd(block_end))
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let local = EntityTransform::Translate((-base).as_dvec3()); let local = EntityTransform::Translate(-base);
for mut entity in entities { for mut entity in entities {
view::dispatch::apply_transform(&mut entity, &local); view::dispatch::apply_transform(&mut entity, &local);
entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity); entity = crate::modules::draw::modify::explode::normalize_entity_for_block(entity);