feat(xref+block): bind xref tables, fade rendered geometry, per-defn precision offset

Xref work:
- merge_xref_into_block now copies the xref's layer / linetype / nested
  block_record tables into the host under `{xref}|{name}` prefixes and
  remaps entity references, so ByLayer/ByBlock resolution lands on the
  right entry instead of falling back to WHITE + 1 px.
- expand_insert (and the explode fallback) detect xref BlockRecords by
  flag and apply `fade_toward_bg(color, bg, 0.5)` to every emitted final
  color (selection bypasses the fade). ExpandCtx propagates `is_xref` +
  `bg_color` through nested expansions.

Block precision groundwork:
- BlockDefn gains `local_offset: [f64; 3]` set in build_defn from the
  AABB centre of the defn's direct entities. tessellate_sub_local now
  passes that offset to tessellate(), so LocalWire.points / fill_tris /
  aabb_local / text_obb_local are stored in offset-frame f32 and don't
  lose precision for distant content.
- emit_wire / emit_greeked_text / transform_tangent take the defn offset
  and re-add it in f64 before composing with accum_xform. defn_aabb_
  recursive transforms nested children from their offset frame into the
  parent's. transform_offset_aabb_xy replaces transform_aabb_xy.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-14 21:45:37 +03:00
1 parent ea87b3db76
commit 2f0220b1a0
3 files changed
+395 -69

No files matched your search

+128 -6
View File
@@ -2,8 +2,10 @@
// populate them with geometry from the referenced DWG/DXF files.
use acadrust::entities::{Block, BlockEnd};
use acadrust::tables::TableEntry;
use acadrust::types::{Handle, Vector3};
use acadrust::{CadDocument, EntityType};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
/// Status of an external reference block.
@@ -61,7 +63,7 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> Vec<XrefInfo> {
Err(_) => XrefStatus::NotFound,
Ok(xref_doc) => {
ensure_block_entities(doc, &block_name);
merge_xref_into_block(doc, br_handle, xref_doc);
merge_xref_into_block(doc, &block_name, br_handle, xref_doc);
XrefStatus::Loaded
}
},
@@ -130,9 +132,91 @@ fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
let _ = doc.add_entity(EntityType::BlockEnd(BlockEnd::new()));
}
/// Copy model-space entities from `xref_doc` into the xref block (`br_handle`)
/// of `doc`.
fn merge_xref_into_block(doc: &mut CadDocument, br_handle: Handle, xref_doc: CadDocument) {
/// Merge an external-reference document into `doc`'s xref block.
///
/// Copies the xref's model-space entities into the host xref block, AND
/// (crucially for correct rendering) merges the xref's layer / linetype
/// tables and any *nested* block records into the host doc under prefixed
/// names ("{xref_name}|{symbol_name}"). Without this remapping, every
/// xref entity using ByLayer resolves against the host doc's layer table
/// — which doesn't know about the xref's layers — and silently falls
/// back to WHITE / 1 px line weight. AutoCAD's BIND command uses the
/// same naming scheme.
fn merge_xref_into_block(
doc: &mut CadDocument,
xref_block_name: &str,
br_handle: Handle,
xref_doc: CadDocument,
) {
let prefix = xref_block_name;
// ── Layers ──────────────────────────────────────────────────────────
// Prefix every xref layer (including "0"). Entity layer references are
// remapped below so the resolver finds the merged copy. Host layers
// (incl. its own "0") are untouched — no collisions.
let mut layer_map: HashMap<String, String> = HashMap::new();
for layer in xref_doc.layers.iter() {
let old = layer.name.clone();
let new = format!("{}|{}", prefix, old);
let mut cloned = layer.clone();
cloned.name = new.clone();
cloned.set_handle(doc.allocate_handle());
doc.layers.add_or_replace(cloned);
layer_map.insert(old.to_uppercase(), new);
}
// ── Linetypes ───────────────────────────────────────────────────────
// Skip the three sentinel names — "ByLayer" / "ByBlock" / "Continuous"
// are magic strings the resolver matches verbatim in both docs.
let mut linetype_map: HashMap<String, String> = HashMap::new();
for lt in xref_doc.line_types.iter() {
let old = lt.name.clone();
if is_sentinel_linetype(&old) {
continue;
}
let new = format!("{}|{}", prefix, old);
let mut cloned = lt.clone();
cloned.name = new.clone();
cloned.set_handle(doc.allocate_handle());
doc.line_types.add_or_replace(cloned);
linetype_map.insert(old.to_uppercase(), new);
}
// ── Block records (nested blocks inside the xref) ───────────────────
// First pass: create each prefixed BR with its host handle + empty
// entity_handles. Second pass (in the entity loop below) populates
// them by inserting entities with `owner_handle` set.
//
// Tracks (xref-doc BR handle → host BR handle) so entities owned by
// a nested xref block can be routed to the right host block_record.
let mut br_handle_map: HashMap<Handle, Handle> = HashMap::new();
let mut block_name_map: HashMap<String, String> = HashMap::new();
for br in xref_doc.block_records.iter() {
// Skip layout block records (*Model_Space, *Paper_Space, *Paper_Space0…)
// and any further-nested xrefs (we don't recurse into xref-of-xref).
if br.name.starts_with('*') || br.flags.is_xref || br.flags.is_xref_overlay {
continue;
}
let old = br.name.clone();
let new = format!("{}|{}", prefix, old);
let mut cloned = br.clone();
cloned.name = new.clone();
cloned.entity_handles.clear();
cloned.insert_handles.clear();
// Detach foreign layout pointer — it refers to a Layout in xref_doc
// we don't import.
cloned.layout = Handle::NULL;
let new_h = doc.allocate_handle();
cloned.set_handle(new_h);
cloned.block_entity_handle = doc.allocate_handle();
cloned.block_end_handle = doc.allocate_handle();
br_handle_map.insert(br.handle, new_h);
block_name_map.insert(old.to_uppercase(), new);
doc.block_records.add_or_replace(cloned);
}
// ── Entities ────────────────────────────────────────────────────────
let xref_ms_handle = xref_doc.header.model_space_block_handle;
let entities: Vec<EntityType> = xref_doc
.entities()
.filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)))
@@ -140,14 +224,52 @@ fn merge_xref_into_block(doc: &mut CadDocument, br_handle: Handle, xref_doc: Cad
.collect();
for mut entity in entities {
// Remap layer / linetype names so the host's resolver hits the
// copies we just inserted.
{
let c = entity.common_mut();
if let Some(new_layer) = layer_map.get(&c.layer.to_uppercase()) {
c.layer = new_layer.clone();
}
if !is_sentinel_linetype(&c.linetype) {
if let Some(new_lt) = linetype_map.get(&c.linetype.to_uppercase()) {
c.linetype = new_lt.clone();
}
}
}
// INSERTs reference blocks by name, not handle — remap.
if let EntityType::Insert(ins) = &mut entity {
if let Some(new_name) = block_name_map.get(&ins.block_name.to_uppercase()) {
ins.block_name = new_name.clone();
}
}
// Route entity to the correct host block record. Entities owned by
// the xref's model_space land in the host xref block (br_handle);
// entities owned by one of the nested BRs land in its prefixed
// counterpart. Paper-space / unknown owners are skipped.
let old_owner = entity.common().owner_handle;
let new_owner = if old_owner == xref_ms_handle {
br_handle
} else if let Some(&h) = br_handle_map.get(&old_owner) {
h
} else {
continue;
};
entity.common_mut().owner_handle = new_owner;
// Clear the foreign handle so acadrust assigns a new one.
set_handle(&mut entity, Handle::NULL);
// Route to the xref block record.
entity.common_mut().owner_handle = br_handle;
let _ = doc.add_entity(entity);
}
}
fn is_sentinel_linetype(name: &str) -> bool {
name.eq_ignore_ascii_case("ByLayer")
|| name.eq_ignore_ascii_case("ByBlock")
|| name.eq_ignore_ascii_case("Continuous")
}
/// Set the handle field of any entity variant.
fn set_handle(entity: &mut EntityType, h: Handle) {
entity.common_mut().handle = h;
+229 -40
View File
@@ -92,8 +92,17 @@ pub struct BlockDefn {
pub subs: Vec<LocalSub>,
/// Union of every sub's local AABB (including nested-INSERT contributions
/// resolved at expand time via their own defn's `aabb_local`). XY only —
/// the wire renderer is 2D-dominant.
/// the wire renderer is 2D-dominant. Expressed in this defn's *offset*
/// frame (i.e. `local_offset` already subtracted).
pub aabb_local: [f32; 4],
/// f64 constant subtracted from every coordinate stored inside this
/// defn (points, AABBs, text OBB corners, fill_tris, snap_pts, …).
/// Picks an origin near the defn's geometric centre so f32 storage
/// retains millimetre precision even when the source content lives at
/// large coordinates (xref data authored at UTM, MGRS, etc.). When the
/// defn is expanded the offset is re-added in f64 BEFORE the Insert
/// transform is applied — that's where precision matters.
pub local_offset: [f64; 3],
}
#[derive(Default, Debug)]
@@ -148,6 +157,15 @@ impl BlockCache {
}
}
/// Returns the union AABB for `block_name`'s defn, expressed in **that
/// defn's offset frame** (so its caller can store it in
/// `BlockDefn.aabb_local` without a coordinate-frame mismatch).
///
/// LocalWire contributions are already in the parent defn's offset
/// frame. Nested-INSERT contributions live in the *child* defn's offset
/// frame, so we re-add `child.local_offset` (f64), apply the nested
/// Insert's transform to get parent-native coordinates, then subtract
/// `parent.local_offset` to land back in the parent's offset frame.
fn defn_aabb_recursive(&self, block_name: &str, visited: &mut Vec<String>) -> [f32; 4] {
if visited.iter().any(|n| n == block_name) {
return [0.0, 0.0, 0.0, 0.0];
@@ -155,6 +173,7 @@ impl BlockCache {
let Some(defn) = self.defns.get(block_name) else {
return [0.0, 0.0, 0.0, 0.0];
};
let parent_lo = defn.local_offset;
visited.push(block_name.to_string());
let mut acc = [0.0_f32, 0.0, 0.0, 0.0];
for sub in &defn.subs {
@@ -162,7 +181,19 @@ impl BlockCache {
LocalSub::Wire(lw) => lw.aabb_local,
LocalSub::Nested(nref) => {
let nested_local = self.defn_aabb_recursive(&nref.block_name, visited);
transform_aabb_xy(nested_local, &nref.xform)
let child_lo = self
.defns
.get(&nref.block_name)
.map(|d| d.local_offset)
.unwrap_or([0.0; 3]);
let parent_native =
transform_offset_aabb_xy(nested_local, child_lo, &nref.xform);
[
parent_native[0] - parent_lo[0] as f32,
parent_native[1] - parent_lo[1] as f32,
parent_native[2] - parent_lo[0] as f32,
parent_native[3] - parent_lo[1] as f32,
]
}
};
acc = aabb_union(acc, aabb);
@@ -214,6 +245,55 @@ fn build_defn(
Some(br) => br,
None => return BlockDefn::default(),
};
// ── Pass 1: compute a defn-level offset near the geometric centre.
// Falling back to [0,0,0] for empty / nested-only defns is fine —
// precision is only at risk when stored points have large magnitudes,
// and those come from direct wire entities.
let mut min = [f64::INFINITY; 3];
let mut max = [f64::NEG_INFINITY; 3];
let mut have_any = false;
for &eh in &br.entity_handles {
let Some(entity) = doc.get_entity(eh) else {
continue;
};
if matches!(
entity,
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_)
| EntityType::Insert(_)
) {
continue;
}
let bb = entity.as_entity().bounding_box();
for i in 0..3 {
let lo = [bb.min.x, bb.min.y, bb.min.z][i];
let hi = [bb.max.x, bb.max.y, bb.max.z][i];
if !lo.is_finite() || !hi.is_finite() {
continue;
}
if lo < min[i] {
min[i] = lo;
}
if hi > max[i] {
max[i] = hi;
}
have_any = true;
}
}
let local_offset: [f64; 3] = if have_any {
[
(min[0] + max[0]) * 0.5,
(min[1] + max[1]) * 0.5,
(min[2] + max[2]) * 0.5,
]
} else {
[0.0; 3]
};
// ── Pass 2: tessellate each sub with the chosen offset so stored
// coordinates fit into f32 without precision loss.
let cap = br.entity_handles.len();
let mut subs: Vec<LocalSub> = Vec::with_capacity(cap);
for &eh in &br.entity_handles {
@@ -228,7 +308,9 @@ fn build_defn(
subs.push(LocalSub::Nested(build_nested_ref(nested_ins, doc, bg_color)));
}
_ => {
if let Some(lw) = tessellate_sub_local(doc, entity, anno_scale, bg_color) {
if let Some(lw) =
tessellate_sub_local(doc, entity, anno_scale, bg_color, local_offset)
{
subs.push(LocalSub::Wire(lw));
}
}
@@ -237,6 +319,7 @@ fn build_defn(
BlockDefn {
subs,
aabb_local: [0.0; 4],
local_offset,
}
}
@@ -268,6 +351,7 @@ fn tessellate_sub_local(
sub: &EntityType,
anno_scale: f32,
bg_color: [f32; 4],
local_offset: [f64; 3],
) -> Option<LocalWire> {
let h = sub.common().handle;
@@ -286,8 +370,11 @@ fn tessellate_sub_local(
let lt_is_byblock = sub.common().linetype.eq_ignore_ascii_case("byblock");
let lw_is_byblock = matches!(sub.common().line_weight, LineWeight::ByBlock);
// Pass `local_offset` as the f64 world-offset so tessellate subtracts it
// before casting to f32 — same precision-preservation trick used for
// top-level entities, applied per-defn.
let wire = tessellate::tessellate(
doc, h, sub, false, sub_color, pat_len, pat, lw_px, [0.0; 3], anno_scale,
doc, h, sub, false, sub_color, pat_len, pat, lw_px, local_offset, anno_scale,
);
if wire.points.len() > 100_000 {
@@ -304,16 +391,18 @@ fn tessellate_sub_local(
_ => None,
};
// Pre-compute the OBB corners (block-local f32) for Text / MText so the
// greek path can emit a rect that matches the entity's rotation instead
// of falling back to the axis-aligned `aabb_local`.
// Pre-compute the OBB corners (block-offset-frame f32) for Text / MText
// so the greek path can emit a rect that matches the entity's rotation
// instead of falling back to the axis-aligned `aabb_local`. Subtract the
// defn's `local_offset` in f64 first so distant text retains precision.
let [lo_x, lo_y, lo_z] = local_offset;
let text_obb_local: Option<[[f32; 3]; 4]> =
crate::scene::text_obb_corners_native(sub, anno_scale).map(|c| {
[
[c[0][0] as f32, c[0][1] as f32, c[0][2] as f32],
[c[1][0] as f32, c[1][1] as f32, c[1][2] as f32],
[c[2][0] as f32, c[2][1] as f32, c[2][2] as f32],
[c[3][0] as f32, c[3][1] as f32, c[3][2] as f32],
[(c[0][0] - lo_x) as f32, (c[0][1] - lo_y) as f32, (c[0][2] - lo_z) as f32],
[(c[1][0] - lo_x) as f32, (c[1][1] - lo_y) as f32, (c[1][2] - lo_z) as f32],
[(c[2][0] - lo_x) as f32, (c[2][1] - lo_y) as f32, (c[2][2] - lo_z) as f32],
[(c[3][0] - lo_x) as f32, (c[3][1] - lo_y) as f32, (c[3][2] - lo_z) as f32],
]
});
@@ -368,16 +457,19 @@ fn aabb_from_points_iter<I: IntoIterator<Item = [f32; 3]>>(pts: I) -> [f32; 4] {
}
/// Transform a local-space XY AABB by `t` and return the world-space XY AABB
/// of the transformed corners. For non-rotated transforms the corners stay
/// axis-aligned; for arbitrary OCS we still get a correct (looser) AABB by
/// taking the min/max of the four transformed corner points.
fn transform_aabb_xy(local: [f32; 4], t: &Transform) -> [f32; 4] {
/// of the transformed corners. Re-adds an f64 `defn_lo` to each corner
/// before transforming — when the input AABB is stored in a defn's *offset
/// frame* (precision-preservation trick) the offset has to be applied in
/// f64 to keep the world result accurate even for distant content. Pass
/// `[0.0; 3]` for callers that don't use an offset frame.
fn transform_offset_aabb_xy(local: [f32; 4], defn_lo: [f64; 3], t: &Transform) -> [f32; 4] {
let [x0, y0, x1, y1] = local;
let [lo_x, lo_y, lo_z] = defn_lo;
let corners = [
Vector3::new(x0 as f64, y0 as f64, 0.0),
Vector3::new(x1 as f64, y0 as f64, 0.0),
Vector3::new(x1 as f64, y1 as f64, 0.0),
Vector3::new(x0 as f64, y1 as f64, 0.0),
Vector3::new(x0 as f64 + lo_x, y0 as f64 + lo_y, lo_z),
Vector3::new(x1 as f64 + lo_x, y0 as f64 + lo_y, lo_z),
Vector3::new(x1 as f64 + lo_x, y1 as f64 + lo_y, lo_z),
Vector3::new(x0 as f64 + lo_x, y1 as f64 + lo_y, lo_z),
];
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
@@ -441,6 +533,11 @@ pub fn expand_insert(
// World units per screen pixel. When `Some`, wires whose AABB projects
// smaller than `MIN_PIXEL_SIZE` get skipped entirely (LOD).
world_per_pixel: Option<f32>,
// True when `ins.block_name` resolves to an xref BlockRecord. All emitted
// colors are faded toward `bg_color` so xrefs are visually distinguishable
// from native content.
is_xref: bool,
bg_color: [f32; 4],
) -> Option<Vec<WireModel>> {
let defn = cache.defn(&ins.block_name)?;
let xform = ins.get_transform();
@@ -449,7 +546,10 @@ pub fn expand_insert(
let mut visited: Vec<String> = Vec::with_capacity(8);
let [ox, oy, _] = world_offset;
let insert_world = transform_aabb_xy(defn.aabb_local, &xform);
// `defn.aabb_local` is in the defn's offset frame — re-add
// `defn.local_offset` (f64) before transforming so the world AABB is
// accurate for distant content.
let insert_world = transform_offset_aabb_xy(defn.aabb_local, defn.local_offset, &xform);
let insert_local = [
insert_world[0] - ox as f32,
insert_world[1] - oy as f32,
@@ -491,6 +591,8 @@ pub fn expand_insert(
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
bg_color,
};
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0);
}
@@ -506,16 +608,23 @@ fn emit_greeked_text(
lw: &LocalWire,
local_aabb: [f32; 4],
accum_xform: &Transform,
defn_lo: [f64; 3],
ctx: &ExpandCtx,
out: &mut Batches,
) {
let [ox, oy, oz] = ctx.world_offset;
let [lo_x, lo_y, lo_z] = defn_lo;
let tris: [[f32; 3]; 6] = if let Some(obb) = lw.text_obb_local {
// Transform block-local corners → world via accum_xform, then
// subtract world_offset so the fill_tris sit in the same f32 space
// as everything else in the batch.
// Re-add the defn's `local_offset` in f64 before composing with
// `accum_xform` (the OBB corners are stored in offset frame f32 so
// they don't lose precision for distant text). Then subtract host
// `world_offset` so the fill_tris share the batch's f32 space.
let xf = |p: [f32; 3]| -> [f32; 3] {
let w = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64));
let w = accum_xform.apply(Vector3::new(
p[0] as f64 + lo_x,
p[1] as f64 + lo_y,
p[2] as f64 + lo_z,
));
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
};
let p00 = xf(obb[0]);
@@ -543,6 +652,13 @@ fn emit_greeked_text(
} else {
lw.color
};
// Apply the same xref fade as `emit_wire` BEFORE the face3d 0.45 boost,
// so the final on-screen color (boost × 0.45 dim) lands at the faded value.
let final_color = if ctx.is_xref && !ctx.selected {
fade_toward_bg(final_color, ctx.bg_color)
} else {
final_color
};
let boost = 1.0 / 0.45_f32;
let [r, g, b, a] = final_color;
let greek_color = [
@@ -593,6 +709,24 @@ struct ExpandCtx<'a> {
view_aabb: Option<[f32; 4]>,
// World units per screen pixel. `None` = no pixel-size LOD.
world_per_pixel: Option<f32>,
// True when this expansion descends from an xref INSERT. Causes emitted
// colors to be faded toward `bg_color` so the user can tell at a glance
// which geometry comes from an external reference.
is_xref: bool,
bg_color: [f32; 4],
}
/// Fade `color` toward `bg` by 50%, preserving alpha. Used to mark xref
/// geometry — the hue stays recognizable but the contrast against the
/// background drops, reading as "washed out".
pub(super) fn fade_toward_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
const T: f32 = 0.5;
[
color[0] * (1.0 - T) + bg[0] * T,
color[1] * (1.0 - T) + bg[1] * T,
color[2] * (1.0 - T) + bg[2] * T,
color[3],
]
}
/// Style fingerprint used to group local wires into a single GPU buffer.
@@ -735,10 +869,14 @@ fn expand_defn(
eprintln!("block_cache: nested-block depth > {MAX_NESTING_DEPTH}, truncating");
return;
}
let defn_lo = defn.local_offset;
for sub in &defn.subs {
match sub {
LocalSub::Wire(lw) => {
let world = transform_aabb_xy(lw.aabb_local, accum_xform);
// `lw.aabb_local` is in the defn's offset frame; re-add
// `defn_lo` (in f64) before composing with `accum_xform`
// so culling uses correct world-space corners.
let world = transform_offset_aabb_xy(lw.aabb_local, defn_lo, accum_xform);
let [ox, oy, _] = ctx.world_offset;
let local = [
world[0] - ox as f32,
@@ -771,12 +909,12 @@ fn expand_defn(
continue;
}
if h_px < 4.0 {
emit_greeked_text(lw, local, accum_xform, ctx, out);
emit_greeked_text(lw, local, accum_xform, defn_lo, ctx, out);
continue;
}
}
}
emit_wire(lw, accum_xform, ctx, out);
emit_wire(lw, accum_xform, defn_lo, ctx, out);
}
LocalSub::Nested(nref) => {
if visited.iter().any(|n| n == &nref.block_name) {
@@ -788,8 +926,15 @@ fn expand_defn(
};
// Nested-INSERT cull: union AABB of the nested defn,
// transformed by composed xform, vs view rect + pixel size.
// `nested_defn.aabb_local` lives in the nested defn's offset
// frame — re-add `nested_defn.local_offset` in f64 before
// composing with the parent transforms.
let composed = nref.xform.then(accum_xform);
let world = transform_aabb_xy(nested_defn.aabb_local, &composed);
let world = transform_offset_aabb_xy(
nested_defn.aabb_local,
nested_defn.local_offset,
&composed,
);
let [ox, oy, _] = ctx.world_offset;
let local = [
world[0] - ox as f32,
@@ -834,6 +979,8 @@ fn expand_defn(
pslt_factor: ctx.pslt_factor,
view_aabb: ctx.view_aabb,
world_per_pixel: ctx.world_per_pixel,
is_xref: ctx.is_xref,
bg_color: ctx.bg_color,
};
visited.push(nref.block_name.clone());
for offset in &nref.instance_offsets {
@@ -860,11 +1007,18 @@ fn expand_defn(
}
}
fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut Batches) {
fn emit_wire(
lw: &LocalWire,
accum_xform: &Transform,
defn_lo: [f64; 3],
ctx: &ExpandCtx,
out: &mut Batches,
) {
if lw.points.is_empty() && lw.fill_tris.is_empty() {
return;
}
let [ox, oy, oz] = ctx.world_offset;
let [lo_x, lo_y, lo_z] = defn_lo;
// Resolve final style for this LocalWire against the outer Insert ctx
// before we hash it into a batch.
@@ -875,6 +1029,11 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
} else {
lw.color
};
let final_color = if ctx.is_xref && !ctx.selected {
fade_toward_bg(final_color, ctx.bg_color)
} else {
final_color
};
let (final_pat_len, final_pat) = if lw.lt_is_byblock {
(ctx.ins_pat_len, ctx.ins_pat)
} else {
@@ -931,7 +1090,11 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
entry.points.push([f32::NAN; 3]);
continue;
}
let v = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64));
let v = accum_xform.apply(Vector3::new(
p[0] as f64 + lo_x,
p[1] as f64 + lo_y,
p[2] as f64 + lo_z,
));
let q = [(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32];
if q[0] < entry.min_x {
entry.min_x = q[0];
@@ -950,13 +1113,21 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
}
for p in &lw.key_vertices {
let v = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64));
let v = accum_xform.apply(Vector3::new(
p[0] as f64 + lo_x,
p[1] as f64 + lo_y,
p[2] as f64 + lo_z,
));
entry
.key_vertices
.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
}
for (p, hint) in &lw.snap_pts {
let v = accum_xform.apply(Vector3::new(p.x as f64, p.y as f64, p.z as f64));
let v = accum_xform.apply(Vector3::new(
p.x as f64 + lo_x,
p.y as f64 + lo_y,
p.z as f64 + lo_z,
));
entry.snap_pts.push((
glam::Vec3::new(
(v.x - ox) as f32,
@@ -969,10 +1140,14 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
for tg in &lw.tangent_geoms {
entry
.tangent_geoms
.push(transform_tangent(tg, accum_xform, [ox, oy, oz]));
.push(transform_tangent(tg, accum_xform, defn_lo, [ox, oy, oz]));
}
for p in &lw.fill_tris {
let v = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64));
let v = accum_xform.apply(Vector3::new(
p[0] as f64 + lo_x,
p[1] as f64 + lo_y,
p[2] as f64 + lo_z,
));
entry
.fill_tris
.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
@@ -980,12 +1155,26 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
}
fn transform_tangent(tg: &TangentGeom, t: &Transform, woff: [f64; 3]) -> TangentGeom {
fn transform_tangent(
tg: &TangentGeom,
t: &Transform,
defn_lo: [f64; 3],
woff: [f64; 3],
) -> TangentGeom {
let [ox, oy, oz] = woff;
let [lo_x, lo_y, lo_z] = defn_lo;
match tg {
TangentGeom::Line { p1, p2 } => {
let q1 = t.apply(Vector3::new(p1[0] as f64, p1[1] as f64, p1[2] as f64));
let q2 = t.apply(Vector3::new(p2[0] as f64, p2[1] as f64, p2[2] as f64));
let q1 = t.apply(Vector3::new(
p1[0] as f64 + lo_x,
p1[1] as f64 + lo_y,
p1[2] as f64 + lo_z,
));
let q2 = t.apply(Vector3::new(
p2[0] as f64 + lo_x,
p2[1] as f64 + lo_y,
p2[2] as f64 + lo_z,
));
TangentGeom::Line {
p1: [(q1.x - ox) as f32, (q1.y - oy) as f32, (q1.z - oz) as f32],
p2: [(q2.x - ox) as f32, (q2.y - oy) as f32, (q2.z - oz) as f32],
@@ -993,9 +1182,9 @@ fn transform_tangent(tg: &TangentGeom, t: &Transform, woff: [f64; 3]) -> Tangent
}
TangentGeom::Circle { center, radius } => {
let c = t.apply(Vector3::new(
center[0] as f64,
center[1] as f64,
center[2] as f64,
center[0] as f64 + lo_x,
center[1] as f64 + lo_y,
center[2] as f64 + lo_z,
));
let m = &t.matrix.m;
let sx = ((m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]) as f64).sqrt();
+38 -23
View File
@@ -79,24 +79,6 @@ pub struct DerivedCaches {
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
/// Intended to run on a background thread during file load.
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
// world_offset
let h = &doc.header;
let (min, max) = (h.model_space_extents_min, h.model_space_extents_max);
let valid = min.x < max.x && min.y < max.y;
let (world_offset, local_extent_max) = if valid {
let offset = [
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
(min.z + max.z) * 0.5,
];
let hw = ((max.x - min.x) * 0.5) as f32;
let hh = ((max.y - min.y) * 0.5) as f32;
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
(offset, hw.max(hh).max(hz) * 10.0)
} else {
([0.0; 3], 1e9_f32)
};
// model-space block handle (same logic as Scene::model_space_block_handle)
let model_block = doc
.objects
@@ -119,6 +101,25 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
.unwrap_or(Handle::NULL)
});
// world_offset (from DXF header extents — fast and works when the
// writer correctly folded Insert→Block expansion into $EXTMIN/$EXTMAX).
let h = &doc.header;
let (min, max) = (h.model_space_extents_min, h.model_space_extents_max);
let valid = min.x < max.x && min.y < max.y;
let (world_offset, local_extent_max) = if valid {
let offset = [
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
(min.z + max.z) * 0.5,
];
let hw = ((max.x - min.x) * 0.5) as f32;
let hh = ((max.y - min.y) * 0.5) as f32;
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
(offset, hw.max(hh).max(hz) * 10.0)
} else {
([0.0; 3], 1e9_f32)
};
use rayon::prelude::*;
// Single pass over entities — collect only handles per kind. No clones.
@@ -4030,6 +4031,13 @@ fn tessellate_entity(
};
if let Some(cache) = block_cache {
// Xrefs render with the same hue but faded toward `bg_color` so
// the user can recognise external-reference geometry at a glance.
let is_xref = document
.block_records
.get(&ins.block_name)
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.unwrap_or(false);
if let Some(mut wires) = block_cache::expand_insert(
cache,
ins,
@@ -4043,6 +4051,8 @@ fn tessellate_entity(
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
bg_color,
) {
wires.push(marker);
return wires;
@@ -4053,11 +4063,11 @@ fn tessellate_entity(
// but keep the safety bail-out for pathologically large blocks so
// the UI doesn't hang on broken or unbounded sub-entity counts.
const INSERT_SUB_LIMIT: usize = 5_000;
let sub_count = document
.block_records
.get(&ins.block_name)
.map(|br| br.entity_handles.len())
.unwrap_or(0);
let br = document.block_records.get(&ins.block_name);
let sub_count = br.map(|br| br.entity_handles.len()).unwrap_or(0);
let is_xref = br
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.unwrap_or(false);
if sub_count > INSERT_SUB_LIMIT {
return vec![marker];
}
@@ -4077,6 +4087,11 @@ fn tessellate_entity(
ins_lw_px,
);
let sub_color = render::adapt_to_bg(sub_color, bg_color);
let sub_color = if is_xref && !sel {
block_cache::fade_toward_bg(sub_color, bg_color)
} else {
sub_color
};
let sub_aabb = entity_aabb(&sub, world_offset);
let sub_pattern_length = sub_pattern_length * pslt_factor;
let sub_pattern = sub_pattern.map(|v| v * pslt_factor);