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 <noreply@anthropic.com>
This commit is contained in:
parent
ea87b3db76
commit
2f0220b1a0
3 changed files with 395 additions and 69 deletions
134
src/io/xref.rs
134
src/io/xref.rs
|
|
@ -2,8 +2,10 @@
|
||||||
// populate them with geometry from the referenced DWG/DXF files.
|
// populate them with geometry from the referenced DWG/DXF files.
|
||||||
|
|
||||||
use acadrust::entities::{Block, BlockEnd};
|
use acadrust::entities::{Block, BlockEnd};
|
||||||
|
use acadrust::tables::TableEntry;
|
||||||
use acadrust::types::{Handle, Vector3};
|
use acadrust::types::{Handle, Vector3};
|
||||||
use acadrust::{CadDocument, EntityType};
|
use acadrust::{CadDocument, EntityType};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Status of an external reference block.
|
/// 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,
|
Err(_) => XrefStatus::NotFound,
|
||||||
Ok(xref_doc) => {
|
Ok(xref_doc) => {
|
||||||
ensure_block_entities(doc, &block_name);
|
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
|
XrefStatus::Loaded
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -130,9 +132,91 @@ fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||||
let _ = doc.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
let _ = doc.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy model-space entities from `xref_doc` into the xref block (`br_handle`)
|
/// Merge an external-reference document into `doc`'s xref block.
|
||||||
/// of `doc`.
|
///
|
||||||
fn merge_xref_into_block(doc: &mut CadDocument, br_handle: Handle, xref_doc: CadDocument) {
|
/// 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
|
let entities: Vec<EntityType> = xref_doc
|
||||||
.entities()
|
.entities()
|
||||||
.filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)))
|
.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();
|
.collect();
|
||||||
|
|
||||||
for mut entity in entities {
|
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.
|
// Clear the foreign handle so acadrust assigns a new one.
|
||||||
set_handle(&mut entity, Handle::NULL);
|
set_handle(&mut entity, Handle::NULL);
|
||||||
// Route to the xref block record.
|
|
||||||
entity.common_mut().owner_handle = br_handle;
|
|
||||||
let _ = doc.add_entity(entity);
|
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.
|
/// Set the handle field of any entity variant.
|
||||||
fn set_handle(entity: &mut EntityType, h: Handle) {
|
fn set_handle(entity: &mut EntityType, h: Handle) {
|
||||||
entity.common_mut().handle = h;
|
entity.common_mut().handle = h;
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,17 @@ pub struct BlockDefn {
|
||||||
pub subs: Vec<LocalSub>,
|
pub subs: Vec<LocalSub>,
|
||||||
/// Union of every sub's local AABB (including nested-INSERT contributions
|
/// Union of every sub's local AABB (including nested-INSERT contributions
|
||||||
/// resolved at expand time via their own defn's `aabb_local`). XY only —
|
/// 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],
|
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)]
|
#[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] {
|
fn defn_aabb_recursive(&self, block_name: &str, visited: &mut Vec<String>) -> [f32; 4] {
|
||||||
if visited.iter().any(|n| n == block_name) {
|
if visited.iter().any(|n| n == block_name) {
|
||||||
return [0.0, 0.0, 0.0, 0.0];
|
return [0.0, 0.0, 0.0, 0.0];
|
||||||
|
|
@ -155,6 +173,7 @@ impl BlockCache {
|
||||||
let Some(defn) = self.defns.get(block_name) else {
|
let Some(defn) = self.defns.get(block_name) else {
|
||||||
return [0.0, 0.0, 0.0, 0.0];
|
return [0.0, 0.0, 0.0, 0.0];
|
||||||
};
|
};
|
||||||
|
let parent_lo = defn.local_offset;
|
||||||
visited.push(block_name.to_string());
|
visited.push(block_name.to_string());
|
||||||
let mut acc = [0.0_f32, 0.0, 0.0, 0.0];
|
let mut acc = [0.0_f32, 0.0, 0.0, 0.0];
|
||||||
for sub in &defn.subs {
|
for sub in &defn.subs {
|
||||||
|
|
@ -162,7 +181,19 @@ impl BlockCache {
|
||||||
LocalSub::Wire(lw) => lw.aabb_local,
|
LocalSub::Wire(lw) => lw.aabb_local,
|
||||||
LocalSub::Nested(nref) => {
|
LocalSub::Nested(nref) => {
|
||||||
let nested_local = self.defn_aabb_recursive(&nref.block_name, visited);
|
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);
|
acc = aabb_union(acc, aabb);
|
||||||
|
|
@ -214,6 +245,55 @@ fn build_defn(
|
||||||
Some(br) => br,
|
Some(br) => br,
|
||||||
None => return BlockDefn::default(),
|
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 cap = br.entity_handles.len();
|
||||||
let mut subs: Vec<LocalSub> = Vec::with_capacity(cap);
|
let mut subs: Vec<LocalSub> = Vec::with_capacity(cap);
|
||||||
for &eh in &br.entity_handles {
|
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)));
|
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));
|
subs.push(LocalSub::Wire(lw));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -237,6 +319,7 @@ fn build_defn(
|
||||||
BlockDefn {
|
BlockDefn {
|
||||||
subs,
|
subs,
|
||||||
aabb_local: [0.0; 4],
|
aabb_local: [0.0; 4],
|
||||||
|
local_offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,6 +351,7 @@ fn tessellate_sub_local(
|
||||||
sub: &EntityType,
|
sub: &EntityType,
|
||||||
anno_scale: f32,
|
anno_scale: f32,
|
||||||
bg_color: [f32; 4],
|
bg_color: [f32; 4],
|
||||||
|
local_offset: [f64; 3],
|
||||||
) -> Option<LocalWire> {
|
) -> Option<LocalWire> {
|
||||||
let h = sub.common().handle;
|
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 lt_is_byblock = sub.common().linetype.eq_ignore_ascii_case("byblock");
|
||||||
let lw_is_byblock = matches!(sub.common().line_weight, LineWeight::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(
|
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 {
|
if wire.points.len() > 100_000 {
|
||||||
|
|
@ -304,16 +391,18 @@ fn tessellate_sub_local(
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre-compute the OBB corners (block-local f32) for Text / MText so the
|
// Pre-compute the OBB corners (block-offset-frame f32) for Text / MText
|
||||||
// greek path can emit a rect that matches the entity's rotation instead
|
// so the greek path can emit a rect that matches the entity's rotation
|
||||||
// of falling back to the axis-aligned `aabb_local`.
|
// 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]> =
|
let text_obb_local: Option<[[f32; 3]; 4]> =
|
||||||
crate::scene::text_obb_corners_native(sub, anno_scale).map(|c| {
|
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[0][0] - lo_x) as f32, (c[0][1] - lo_y) as f32, (c[0][2] - lo_z) as f32],
|
||||||
[c[1][0] as f32, c[1][1] as f32, c[1][2] 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] as f32, c[2][1] as f32, c[2][2] 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] as f32, c[3][1] as f32, c[3][2] 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
|
/// 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
|
/// of the transformed corners. Re-adds an f64 `defn_lo` to each corner
|
||||||
/// axis-aligned; for arbitrary OCS we still get a correct (looser) AABB by
|
/// before transforming — when the input AABB is stored in a defn's *offset
|
||||||
/// taking the min/max of the four transformed corner points.
|
/// frame* (precision-preservation trick) the offset has to be applied in
|
||||||
fn transform_aabb_xy(local: [f32; 4], t: &Transform) -> [f32; 4] {
|
/// 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 [x0, y0, x1, y1] = local;
|
||||||
|
let [lo_x, lo_y, lo_z] = defn_lo;
|
||||||
let corners = [
|
let corners = [
|
||||||
Vector3::new(x0 as f64, y0 as f64, 0.0),
|
Vector3::new(x0 as f64 + lo_x, y0 as f64 + lo_y, lo_z),
|
||||||
Vector3::new(x1 as f64, y0 as f64, 0.0),
|
Vector3::new(x1 as f64 + lo_x, y0 as f64 + lo_y, lo_z),
|
||||||
Vector3::new(x1 as f64, y1 as f64, 0.0),
|
Vector3::new(x1 as f64 + lo_x, y1 as f64 + lo_y, lo_z),
|
||||||
Vector3::new(x0 as f64, y1 as f64, 0.0),
|
Vector3::new(x0 as f64 + lo_x, y1 as f64 + lo_y, lo_z),
|
||||||
];
|
];
|
||||||
let mut min_x = f64::INFINITY;
|
let mut min_x = f64::INFINITY;
|
||||||
let mut min_y = 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
|
// World units per screen pixel. When `Some`, wires whose AABB projects
|
||||||
// smaller than `MIN_PIXEL_SIZE` get skipped entirely (LOD).
|
// smaller than `MIN_PIXEL_SIZE` get skipped entirely (LOD).
|
||||||
world_per_pixel: Option<f32>,
|
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>> {
|
) -> Option<Vec<WireModel>> {
|
||||||
let defn = cache.defn(&ins.block_name)?;
|
let defn = cache.defn(&ins.block_name)?;
|
||||||
let xform = ins.get_transform();
|
let xform = ins.get_transform();
|
||||||
|
|
@ -449,7 +546,10 @@ pub fn expand_insert(
|
||||||
let mut visited: Vec<String> = Vec::with_capacity(8);
|
let mut visited: Vec<String> = Vec::with_capacity(8);
|
||||||
let [ox, oy, _] = world_offset;
|
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 = [
|
let insert_local = [
|
||||||
insert_world[0] - ox as f32,
|
insert_world[0] - ox as f32,
|
||||||
insert_world[1] - oy as f32,
|
insert_world[1] - oy as f32,
|
||||||
|
|
@ -491,6 +591,8 @@ pub fn expand_insert(
|
||||||
pslt_factor,
|
pslt_factor,
|
||||||
view_aabb,
|
view_aabb,
|
||||||
world_per_pixel,
|
world_per_pixel,
|
||||||
|
is_xref,
|
||||||
|
bg_color,
|
||||||
};
|
};
|
||||||
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0);
|
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -506,16 +608,23 @@ fn emit_greeked_text(
|
||||||
lw: &LocalWire,
|
lw: &LocalWire,
|
||||||
local_aabb: [f32; 4],
|
local_aabb: [f32; 4],
|
||||||
accum_xform: &Transform,
|
accum_xform: &Transform,
|
||||||
|
defn_lo: [f64; 3],
|
||||||
ctx: &ExpandCtx,
|
ctx: &ExpandCtx,
|
||||||
out: &mut Batches,
|
out: &mut Batches,
|
||||||
) {
|
) {
|
||||||
let [ox, oy, oz] = ctx.world_offset;
|
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 {
|
let tris: [[f32; 3]; 6] = if let Some(obb) = lw.text_obb_local {
|
||||||
// Transform block-local corners → world via accum_xform, then
|
// Re-add the defn's `local_offset` in f64 before composing with
|
||||||
// subtract world_offset so the fill_tris sit in the same f32 space
|
// `accum_xform` (the OBB corners are stored in offset frame f32 so
|
||||||
// as everything else in the batch.
|
// 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 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]
|
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
|
||||||
};
|
};
|
||||||
let p00 = xf(obb[0]);
|
let p00 = xf(obb[0]);
|
||||||
|
|
@ -543,6 +652,13 @@ fn emit_greeked_text(
|
||||||
} else {
|
} else {
|
||||||
lw.color
|
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 boost = 1.0 / 0.45_f32;
|
||||||
let [r, g, b, a] = final_color;
|
let [r, g, b, a] = final_color;
|
||||||
let greek_color = [
|
let greek_color = [
|
||||||
|
|
@ -593,6 +709,24 @@ struct ExpandCtx<'a> {
|
||||||
view_aabb: Option<[f32; 4]>,
|
view_aabb: Option<[f32; 4]>,
|
||||||
// World units per screen pixel. `None` = no pixel-size LOD.
|
// World units per screen pixel. `None` = no pixel-size LOD.
|
||||||
world_per_pixel: Option<f32>,
|
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.
|
/// 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");
|
eprintln!("block_cache: nested-block depth > {MAX_NESTING_DEPTH}, truncating");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let defn_lo = defn.local_offset;
|
||||||
for sub in &defn.subs {
|
for sub in &defn.subs {
|
||||||
match sub {
|
match sub {
|
||||||
LocalSub::Wire(lw) => {
|
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 [ox, oy, _] = ctx.world_offset;
|
||||||
let local = [
|
let local = [
|
||||||
world[0] - ox as f32,
|
world[0] - ox as f32,
|
||||||
|
|
@ -771,12 +909,12 @@ fn expand_defn(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if h_px < 4.0 {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit_wire(lw, accum_xform, ctx, out);
|
emit_wire(lw, accum_xform, defn_lo, ctx, out);
|
||||||
}
|
}
|
||||||
LocalSub::Nested(nref) => {
|
LocalSub::Nested(nref) => {
|
||||||
if visited.iter().any(|n| n == &nref.block_name) {
|
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,
|
// Nested-INSERT cull: union AABB of the nested defn,
|
||||||
// transformed by composed xform, vs view rect + pixel size.
|
// 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 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 [ox, oy, _] = ctx.world_offset;
|
||||||
let local = [
|
let local = [
|
||||||
world[0] - ox as f32,
|
world[0] - ox as f32,
|
||||||
|
|
@ -834,6 +979,8 @@ fn expand_defn(
|
||||||
pslt_factor: ctx.pslt_factor,
|
pslt_factor: ctx.pslt_factor,
|
||||||
view_aabb: ctx.view_aabb,
|
view_aabb: ctx.view_aabb,
|
||||||
world_per_pixel: ctx.world_per_pixel,
|
world_per_pixel: ctx.world_per_pixel,
|
||||||
|
is_xref: ctx.is_xref,
|
||||||
|
bg_color: ctx.bg_color,
|
||||||
};
|
};
|
||||||
visited.push(nref.block_name.clone());
|
visited.push(nref.block_name.clone());
|
||||||
for offset in &nref.instance_offsets {
|
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() {
|
if lw.points.is_empty() && lw.fill_tris.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let [ox, oy, oz] = ctx.world_offset;
|
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
|
// Resolve final style for this LocalWire against the outer Insert ctx
|
||||||
// before we hash it into a batch.
|
// before we hash it into a batch.
|
||||||
|
|
@ -875,6 +1029,11 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
|
||||||
} else {
|
} else {
|
||||||
lw.color
|
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 {
|
let (final_pat_len, final_pat) = if lw.lt_is_byblock {
|
||||||
(ctx.ins_pat_len, ctx.ins_pat)
|
(ctx.ins_pat_len, ctx.ins_pat)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -931,7 +1090,11 @@ fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut
|
||||||
entry.points.push([f32::NAN; 3]);
|
entry.points.push([f32::NAN; 3]);
|
||||||
continue;
|
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];
|
let q = [(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32];
|
||||||
if q[0] < entry.min_x {
|
if q[0] < entry.min_x {
|
||||||
entry.min_x = q[0];
|
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 {
|
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
|
entry
|
||||||
.key_vertices
|
.key_vertices
|
||||||
.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
|
.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
|
||||||
}
|
}
|
||||||
for (p, hint) in &lw.snap_pts {
|
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((
|
entry.snap_pts.push((
|
||||||
glam::Vec3::new(
|
glam::Vec3::new(
|
||||||
(v.x - ox) as f32,
|
(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 {
|
for tg in &lw.tangent_geoms {
|
||||||
entry
|
entry
|
||||||
.tangent_geoms
|
.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 {
|
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
|
entry
|
||||||
.fill_tris
|
.fill_tris
|
||||||
.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
|
.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 [ox, oy, oz] = woff;
|
||||||
|
let [lo_x, lo_y, lo_z] = defn_lo;
|
||||||
match tg {
|
match tg {
|
||||||
TangentGeom::Line { p1, p2 } => {
|
TangentGeom::Line { p1, p2 } => {
|
||||||
let q1 = t.apply(Vector3::new(p1[0] as f64, p1[1] as f64, p1[2] as f64));
|
let q1 = t.apply(Vector3::new(
|
||||||
let q2 = t.apply(Vector3::new(p2[0] as f64, p2[1] as f64, p2[2] as f64));
|
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 {
|
TangentGeom::Line {
|
||||||
p1: [(q1.x - ox) as f32, (q1.y - oy) as f32, (q1.z - oz) as f32],
|
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],
|
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 } => {
|
TangentGeom::Circle { center, radius } => {
|
||||||
let c = t.apply(Vector3::new(
|
let c = t.apply(Vector3::new(
|
||||||
center[0] as f64,
|
center[0] as f64 + lo_x,
|
||||||
center[1] as f64,
|
center[1] as f64 + lo_y,
|
||||||
center[2] as f64,
|
center[2] as f64 + lo_z,
|
||||||
));
|
));
|
||||||
let m = &t.matrix.m;
|
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();
|
let sx = ((m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]) as f64).sqrt();
|
||||||
|
|
|
||||||
|
|
@ -79,24 +79,6 @@ pub struct DerivedCaches {
|
||||||
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
|
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
|
||||||
/// Intended to run on a background thread during file load.
|
/// Intended to run on a background thread during file load.
|
||||||
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
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)
|
// model-space block handle (same logic as Scene::model_space_block_handle)
|
||||||
let model_block = doc
|
let model_block = doc
|
||||||
.objects
|
.objects
|
||||||
|
|
@ -119,6 +101,25 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
.unwrap_or(Handle::NULL)
|
.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::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
// Single pass over entities — collect only handles per kind. No clones.
|
// Single pass over entities — collect only handles per kind. No clones.
|
||||||
|
|
@ -4030,6 +4031,13 @@ fn tessellate_entity(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(cache) = block_cache {
|
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(
|
if let Some(mut wires) = block_cache::expand_insert(
|
||||||
cache,
|
cache,
|
||||||
ins,
|
ins,
|
||||||
|
|
@ -4043,6 +4051,8 @@ fn tessellate_entity(
|
||||||
pslt_factor,
|
pslt_factor,
|
||||||
view_aabb,
|
view_aabb,
|
||||||
world_per_pixel,
|
world_per_pixel,
|
||||||
|
is_xref,
|
||||||
|
bg_color,
|
||||||
) {
|
) {
|
||||||
wires.push(marker);
|
wires.push(marker);
|
||||||
return wires;
|
return wires;
|
||||||
|
|
@ -4053,11 +4063,11 @@ fn tessellate_entity(
|
||||||
// but keep the safety bail-out for pathologically large blocks so
|
// but keep the safety bail-out for pathologically large blocks so
|
||||||
// the UI doesn't hang on broken or unbounded sub-entity counts.
|
// the UI doesn't hang on broken or unbounded sub-entity counts.
|
||||||
const INSERT_SUB_LIMIT: usize = 5_000;
|
const INSERT_SUB_LIMIT: usize = 5_000;
|
||||||
let sub_count = document
|
let br = document.block_records.get(&ins.block_name);
|
||||||
.block_records
|
let sub_count = br.map(|br| br.entity_handles.len()).unwrap_or(0);
|
||||||
.get(&ins.block_name)
|
let is_xref = br
|
||||||
.map(|br| br.entity_handles.len())
|
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||||
.unwrap_or(0);
|
.unwrap_or(false);
|
||||||
if sub_count > INSERT_SUB_LIMIT {
|
if sub_count > INSERT_SUB_LIMIT {
|
||||||
return vec![marker];
|
return vec![marker];
|
||||||
}
|
}
|
||||||
|
|
@ -4077,6 +4087,11 @@ fn tessellate_entity(
|
||||||
ins_lw_px,
|
ins_lw_px,
|
||||||
);
|
);
|
||||||
let sub_color = render::adapt_to_bg(sub_color, bg_color);
|
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_aabb = entity_aabb(&sub, world_offset);
|
||||||
let sub_pattern_length = sub_pattern_length * pslt_factor;
|
let sub_pattern_length = sub_pattern_length * pslt_factor;
|
||||||
let sub_pattern = sub_pattern.map(|v| v * pslt_factor);
|
let sub_pattern = sub_pattern.map(|v| v * pslt_factor);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue