fix(blocks): unify nested object transforms

Route block-backed entities through one local geometry and accumulated transform path.

Refs #864
This commit is contained in:
Hakan Seven committed 2026-08-30 23:30:14 +03:00
1 parent 853e830126
commit abb4f4bf56
4 files changed
+471 -587

No files matched your search

+80 -111
View File
@@ -1700,6 +1700,59 @@ impl crate::entities::traits::Transformable for MultiLeader {
}
}
pub(crate) fn block_content_insert(
document: &acadrust::CadDocument,
ml: &MultiLeader,
) -> Option<acadrust::entities::Insert> {
if ml.content_type != LeaderContentType::Block || !ml.context.has_block_contents {
return None;
}
let record = ml.block_content_handle.and_then(|handle| {
document
.block_records
.iter()
.find(|record| record.handle == handle)
})?;
let mut insertion = ml.context.block_content_location;
if ml.block_connection_type == BlockContentConnectionType::BlockExtents {
let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
for handle in &record.entity_handles {
if let Some(entity) = document.get_entity(*handle) {
let aabb = crate::scene::convert::tess::entity_aabb(entity);
if aabb != crate::scene::model::wire_model::WireModel::UNBOUNDED_AABB {
bounds[0] = bounds[0].min(aabb[0]);
bounds[1] = bounds[1].min(aabb[1]);
bounds[2] = bounds[2].max(aabb[2]);
bounds[3] = bounds[3].max(aabb[3]);
}
}
}
if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] {
let to_block = Transform::rotation(-ml.block_rotation);
let from_left = ml.context.leader_roots.first().map_or(true, |root| {
to_block.apply_vector([root.direction.x, root.direction.y])[0] >= 0.0
});
let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64;
let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64;
let local_x = (anchor_x - record.base_point.x) * ml.block_scale.x;
let local_y = (anchor_y - record.base_point.y) * ml.block_scale.y;
let rotated =
Transform::rotation(ml.block_rotation).apply_vector([local_x, local_y]);
insertion.x -= rotated[0];
insertion.y -= rotated[1];
}
}
let mut insert = acadrust::entities::Insert::new(record.name.clone(), insertion);
insert.set_x_scale(ml.block_scale.x);
insert.set_y_scale(ml.block_scale.y);
insert.set_z_scale(ml.block_scale.z);
insert.rotation = ml.block_rotation;
insert.normal = ml.context.block_content_normal;
insert.common = ml.common.clone();
insert.common.color = ml.block_content_color.clone();
Some(insert)
}
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
/// text strokes, frame, background fill) so each piece can carry its
@@ -1713,6 +1766,10 @@ pub trait MultiLeaderTess {
entity_color: [f32; 4],
line_weight_px: f32,
anno_scale: f32,
active_viewport: Option<acadrust::Handle>,
annotation_scale_handle: Option<acadrust::Handle>,
block_cache: Option<&crate::scene::cache::block_cache::BlockCache>,
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
bg_color: [f32; 4],
) -> Vec<crate::scene::model::wire_model::WireModel>;
@@ -1727,11 +1784,15 @@ impl MultiLeaderTess for MultiLeader {
entity_color: [f32; 4],
line_weight_px: f32,
anno_scale: f32,
_world_per_pixel: Option<f32>,
active_viewport: Option<acadrust::Handle>,
annotation_scale_handle: Option<acadrust::Handle>,
block_cache: Option<&crate::scene::cache::block_cache::BlockCache>,
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
bg_color: [f32; 4],
) -> Vec<crate::scene::model::wire_model::WireModel> {
use crate::scene::convert::tessellate::{
append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom,
append_arrow, arrow_from_block, color_or_inherit, ArrowKind, DimGeom,
};
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
use glam::Vec3;
@@ -2043,117 +2104,25 @@ impl MultiLeaderTess for MultiLeader {
fill_tris_low: Vec::new(),
});
// ── Block content ───────────────────────────────────────────────────────
// When content_type == Block, the MultiLeader displays a block reference
// at block_content_location with the recorded rotation/scale. A
// synthetic Insert supplies that context to the shared scene graph;
// every child then uses its normal tessellator.
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
let block_record = match ml.block_content_handle {
Some(h) if !h.is_null() => document
.block_records
.iter()
.find(|br| br.handle == h)
.cloned(),
_ => None,
};
if let Some(block_record) = block_record {
let block_name = block_record.name.clone();
let block_color = if selected {
line_color
} else {
color_or_inherit(&ml.block_content_color, entity_color)
};
let mut insertion = ml.context.block_content_location;
if ml.block_connection_type == BlockContentConnectionType::BlockExtents {
let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN];
for entity_handle in &block_record.entity_handles {
if let Some(entity) = document.get_entity(*entity_handle) {
let aabb = crate::scene::convert::tess::entity_aabb(entity);
if aabb != WireModel::UNBOUNDED_AABB {
bounds[0] = bounds[0].min(aabb[0]);
bounds[1] = bounds[1].min(aabb[1]);
bounds[2] = bounds[2].max(aabb[2]);
bounds[3] = bounds[3].max(aabb[3]);
}
}
}
if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] {
let to_block = Transform::rotation(-ml.block_rotation);
let from_left = ml
.context
.leader_roots
.first()
.map_or(true, |root| {
to_block.apply_vector([
root.direction.x,
root.direction.y,
])[0]
>= 0.0
});
let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64;
let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64;
let local_x =
(anchor_x - block_record.base_point.x) * ml.block_scale.x;
let local_y =
(anchor_y - block_record.base_point.y) * ml.block_scale.y;
let rotated = Transform::rotation(ml.block_rotation)
.apply_vector([local_x, local_y]);
insertion.x -= rotated[0];
insertion.y -= rotated[1];
}
}
let mut synth_ins = acadrust::entities::Insert::new(block_name, insertion);
synth_ins.set_x_scale(ml.block_scale.x);
synth_ins.set_y_scale(ml.block_scale.y);
synth_ins.set_z_scale(ml.block_scale.z);
synth_ins.rotation = ml.block_rotation;
synth_ins.common.layer = ml.common.layer.clone();
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
if let Some(insert) = block_content_insert(document, ml) {
wires.extend(
crate::scene::convert::tess::expand_block_object(
document,
None,
None,
true,
&depths,
);
graph.walk_insert(
&synth_ins,
&insert,
handle,
|_, _| true,
|entity, context| {
let mut placed = entity.clone();
placed.apply_transform(&context.transform);
let mut sub_wires = tessellate(
document,
handle,
&placed,
selected,
block_color,
leader_pat_len,
leader_pat,
leader_lw_px,
1.0,
None,
None,
bg_color,
false,
);
for w in &mut sub_wires {
w.name = name.clone();
}
wires.extend(sub_wires);
},
);
// Block attributes attached to the multileader — render each as
// its own attribute entity at WCS location like INSERT does.
for ba in &ml.block_attributes {
let _ = ba; // BlockAttribute carries only the value override
// string; we'd need the AttributeDefinition handle
// to materialise it as ATTRIB geometry. Skipped
// until that wiring exists.
}
}
selected,
active_viewport,
bg_color,
1.0,
annotation_scale_handle,
block_cache,
view_aabb,
world_per_pixel,
1.0,
crate::scene::convert::tess::BlockObjectOptions::default(),
)
.wires,
);
}
// ── Text strokes / frame / background fill ──────────────────────────────
+87 -96
View File
@@ -42,6 +42,7 @@ pub struct LocalWire {
/// Low-bit residual paired with `points` so block-instance wires keep
/// sub-f32 precision once the renderer translates them to world space.
pub points_low: Vec<[f32; 3]>,
pub is_point: bool,
pub point_marker: Option<PointMarker>,
/// SDF glyph quads for block-internal text, in block-local coordinates.
/// Non-empty only when SDF text is on and this sub is a TEXT/MTEXT. The
@@ -126,6 +127,7 @@ pub struct NestedRef {
/// The nested INSERT's own signed draw-order rank in (-1,1) within the
/// parent block — narrows the depth sub-range its children may occupy.
pub local_rank: f32,
pub suppress_root_points: bool,
}
#[derive(Clone, Debug)]
@@ -172,6 +174,7 @@ struct ExpansionPrototypeKey {
insert_style: Vec<u32>,
selected: bool,
is_xref: bool,
suppress_root_points: bool,
}
#[derive(Debug)]
@@ -207,11 +210,8 @@ impl BlockCache {
self.defns.get(block_name)
}
/// Build (flat) defns only for block records actually referenced by
/// Inserts in the document — transitively, so nested-insert targets are
/// included too. The Model_Space / Paper_Space block_records are skipped
/// because their entities are emitted as top-level wires, not via the
/// cache.
/// Build definitions referenced by block-backed entities. Space records stay
/// roots so their direct entities preserve individual selection handles.
pub fn build(
doc: &CadDocument,
anno_scale: f32,
@@ -395,16 +395,24 @@ impl BlockCache {
}
}
/// Walk all entities + all block_record contents collecting every distinct
/// `block_name` that appears in an Insert (transitively).
fn block_object_name(doc: &CadDocument, entity: &EntityType) -> Option<String> {
if let EntityType::Insert(insert) = entity {
Some(insert.block_name.clone())
} else {
crate::scene::render_graph::owned_block_insert(doc, entity)
.map(|owned| owned.insert.block_name)
}
}
/// Collect every referenced block-backed object.
fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::default();
let mut queue: Vec<String> = Vec::new();
for entity in doc.entities() {
if let EntityType::Insert(ins) = entity {
if seen.insert(ins.block_name.clone()) {
queue.push(ins.block_name.clone());
if let Some(name) = block_object_name(doc, entity) {
if seen.insert(name.clone()) {
queue.push(name);
}
}
}
@@ -416,9 +424,9 @@ fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
let Some(entity) = doc.get_entity(eh) else {
continue;
};
if let EntityType::Insert(ins) = entity {
if seen.insert(ins.block_name.clone()) {
queue.push(ins.block_name.clone());
if let Some(name) = block_object_name(doc, entity) {
if seen.insert(name.clone()) {
queue.push(name);
}
}
}
@@ -435,11 +443,13 @@ fn referenced_block_tree(doc: &CadDocument, root: &str) -> Vec<String> {
continue;
};
for &handle in &record.entity_handles {
let Some(EntityType::Insert(insert)) = doc.get_entity(handle) else {
let Some(entity) = doc.get_entity(handle) else {
continue;
};
if seen.insert(insert.block_name.clone()) {
queue.push(insert.block_name.clone());
if let Some(name) = block_object_name(doc, entity) {
if seen.insert(name.clone()) {
queue.push(name);
}
}
}
}
@@ -510,6 +520,20 @@ fn build_defn(
) {
continue;
}
if let Some(owned) = crate::scene::render_graph::owned_block_insert(doc, entity) {
if owned.render_picture {
let mut nested = build_nested_ref(
&owned.insert,
doc,
bg_color,
viewport,
depth_map,
);
nested.suppress_root_points = owned.suppress_root_points;
subs.push(LocalSub::Nested(nested));
continue;
}
}
match entity {
EntityType::Block(_) | EntityType::BlockEnd(_) => continue,
// A non-constant ATTDEF is only a template — the insert supplies an
@@ -543,91 +567,30 @@ fn build_defn(
subs.push(LocalSub::Wire(wire));
}
}
// A nested table's stored graphics use the same scene-graph
// traversal as top-level table content. The resulting leaf wires
// remain local to this cached definition; the parent instance
// transform is composed later by the cache expansion.
EntityType::Table(table) => {
let baked = table.block_record_handle.and_then(|handle| {
doc.block_records
.iter()
.find(|record| record.handle == handle)
});
if let Some(record) = baked.filter(|record| {
!record.entity_handles.is_empty()
}) {
let table_plot_l0 = crate::scene::view::render::is_effective_layer_zero(
&table.common.layer,
);
let table_plot_visible = doc
.layers
.get(&table.common.layer)
.map(|layer| layer.is_plottable)
.unwrap_or(true);
let mut insert = acadrust::entities::Insert::new(
record.name.clone(),
table.insertion_point,
);
insert.rotation = table
.horizontal_direction
.y
.atan2(table.horizontal_direction.x);
insert.common = table.common.clone();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
doc,
None,
annotation_scale_handle,
all_visible,
depth_map,
)
.with_viewport(viewport);
graph.walk_insert(
for wire in tessellate_sub_local(
doc,
entity,
anno_scale,
annotation_scale_handle,
bg_color,
viewport,
depth_map,
) {
subs.push(LocalSub::Wire(wire));
}
for insert in crate::entities::table::block_cell_inserts(
table,
doc,
anno_scale,
) {
subs.push(LocalSub::Nested(build_nested_ref(
&insert,
table.common.handle,
|_, _| true,
|leaf, context| {
let mut placed = leaf.clone();
placed.apply_transform(&context.transform);
for mut wire in tessellate_sub_local(
doc,
&placed,
anno_scale,
annotation_scale_handle,
bg_color,
viewport,
depth_map,
) {
wire.plot_visible &= table_plot_l0 || table_plot_visible;
wire.plot_l0 |= table_plot_l0;
subs.push(LocalSub::Wire(wire));
}
},
);
} else {
for wire in tessellate_sub_local(
doc,
entity,
anno_scale,
annotation_scale_handle,
bg_color,
viewport,
depth_map,
) {
subs.push(LocalSub::Wire(wire));
}
for insert in crate::entities::table::block_cell_inserts(
table,
doc,
anno_scale,
) {
subs.push(LocalSub::Nested(build_nested_ref(
&insert,
doc,
bg_color,
viewport,
depth_map,
)));
}
)));
}
}
_ => {
@@ -695,6 +658,7 @@ fn build_nested_ref(
local_rank: depth_map
.get(&nested_ins.common.handle.value())
.map_or(0.0, |d| d[0]),
suppress_root_points: false,
}
}
@@ -850,6 +814,7 @@ fn tessellate_sub_local(
result.push(LocalWire {
points: wire.points,
points_low: wire.points_low,
is_point: matches!(sub, EntityType::Point(_)),
point_marker: wire.point_marker,
text_verts: wire.text_verts,
key_vertices: wire.key_vertices,
@@ -1005,6 +970,9 @@ pub fn expand_insert(
// Current annotation scale. An annotative block scales as one uniform unit
// about its insertion point; a non-annotative block is unaffected.
anno_scale: f32,
// Dimension picture blocks store definition POINTs at their root. They are
// metadata, not visible geometry; nested POINTs remain regular block data.
suppress_root_points: bool,
) -> Option<Vec<WireModel>> {
let defn = cache.defn(&ins.block_name)?;
let base = defn.base_point;
@@ -1045,6 +1013,7 @@ pub fn expand_insert(
is_xref,
bg_color,
anno_scale,
suppress_root_points,
))
} else {
None
@@ -1148,6 +1117,7 @@ pub fn expand_insert(
&mut first_batches,
&mut visited,
0,
suppress_root_points,
(0.0, 1.0),
);
let mut first = first_batches.finalize(&name, selected, bg_color);
@@ -1185,7 +1155,16 @@ pub fn expand_insert(
));
translation.then(&xform)
};
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0));
expand_defn(
defn,
&base_xform,
&ctx,
&mut batches,
&mut visited,
0,
suppress_root_points,
(0.0, 1.0),
);
}
let mut wires = batches.finalize(&name, selected, bg_color);
if let Some(guard) = prototype_guard.as_mut() {
@@ -1230,6 +1209,7 @@ fn expansion_prototype_key(
is_xref: bool,
bg_color: [f32; 4],
anno_scale: f32,
suppress_root_points: bool,
) -> ExpansionPrototypeKey {
let matrix = &transform.matrix.m;
let linear = [
@@ -1262,6 +1242,7 @@ fn expansion_prototype_key(
insert_style,
selected,
is_xref,
suppress_root_points,
}
}
@@ -1384,6 +1365,7 @@ fn nested_prototype_key(
transform: &Transform,
ctx: &ExpandCtx<'_>,
depth_scale: f32,
suppress_root_points: bool,
) -> NestedPrototypeKey {
let matrix = &transform.matrix.m;
let linear = [
@@ -1411,6 +1393,7 @@ fn nested_prototype_key(
selected: ctx.selected,
is_xref: ctx.is_xref,
depth_scale: depth_scale.to_bits(),
suppress_root_points,
}
}
@@ -1527,6 +1510,7 @@ struct NestedPrototypeKey {
selected: bool,
is_xref: bool,
depth_scale: u32,
suppress_root_points: bool,
}
#[derive(Clone, Debug)]
@@ -1766,6 +1750,7 @@ fn expand_defn(
out: &mut Batches,
visited: &mut Vec<String>,
depth: usize,
suppress_root_points: bool,
// Block-local depth sub-range `(base, scale)` accumulated through nested
// inserts: a child at rank r lands at `base + r * scale`, all within
// (-1,1) of the top-level insert. Seeded `(0.0, 1.0)` by `expand_insert`.
@@ -1778,6 +1763,9 @@ fn expand_defn(
for sub in &defn.subs {
match sub {
LocalSub::Wire(lw) => {
if suppress_root_points && lw.is_point {
continue;
}
// `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.
@@ -1926,6 +1914,7 @@ fn expand_defn(
&mut sub,
visited,
depth + 1,
nref.suppress_root_points,
nested_range,
);
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
@@ -1958,6 +1947,7 @@ fn expand_defn(
&composed,
&inner_ctx,
nested_range.1,
nref.suppress_root_points,
);
if let Some(cached) = out.nested_prototypes.get(&key).cloned() {
let delta = [
@@ -1986,6 +1976,7 @@ fn expand_defn(
&mut sub,
visited,
depth + 1,
nref.suppress_root_points,
nested_range,
);
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
+235 -277
View File
@@ -17,6 +17,172 @@ fn fade_if_locked(
}
}
#[derive(Clone, Copy)]
struct BlockObjectStyle {
color: [f32; 4],
aci: u8,
pattern_length: f32,
pattern: [f32; 8],
line_weight_px: f32,
layer: view::render::InheritStyle,
layer_aci: u8,
layer_plottable: bool,
}
pub(crate) struct ExpandedBlockObject {
pub(crate) wires: Vec<WireModel>,
style: BlockObjectStyle,
is_xref: bool,
}
#[derive(Clone, Copy, Default)]
pub(crate) struct BlockObjectOptions {
pub(crate) suppress_root_points: bool,
pub(crate) apply_xclip: bool,
}
fn block_object_style(
document: &acadrust::CadDocument,
insert: &acadrust::entities::Insert,
active_viewport: Option<Handle>,
bg_color: [f32; 4],
) -> BlockObjectStyle {
let entity = EntityType::Insert(insert.clone());
let (color, pattern_length, pattern, line_weight_px, aci) =
view::render::render_style_for_viewport(document, &entity, active_viewport);
let mut layer = view::render::layer_render_style_viewport(
document,
&insert.common.layer,
active_viewport,
);
layer.color = view::render::adapt_to_bg(layer.color, bg_color);
let layer_aci = document
.layers
.get(&insert.common.layer)
.and_then(|layer| match &layer.color {
acadrust::types::Color::Index(index) => Some(*index),
_ => None,
})
.unwrap_or(0);
let layer_plottable = document
.layers
.get(&insert.common.layer)
.map(|layer| layer.is_plottable)
.unwrap_or(true);
BlockObjectStyle {
color: view::render::adapt_to_bg(color, bg_color),
aci,
pattern_length,
pattern,
line_weight_px,
layer,
layer_aci,
layer_plottable,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn expand_block_object(
document: &acadrust::CadDocument,
insert: &acadrust::entities::Insert,
owner: Handle,
selected: bool,
active_viewport: Option<Handle>,
bg_color: [f32; 4],
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
block_cache: Option<&cache::block_cache::BlockCache>,
view_aabb: Option<[f32; 4]>,
world_per_pixel: Option<f32>,
pslt_factor: f32,
options: BlockObjectOptions,
) -> ExpandedBlockObject {
let style = block_object_style(document, insert, active_viewport, bg_color);
let fallback_depths = rustc_hash::FxHashMap::default();
let fallback_cache;
let cache = match block_cache {
Some(cache) if cache.defn(&insert.block_name).is_some() => cache,
_ => {
fallback_cache = cache::block_cache::BlockCache::build_for_block(
document,
&insert.block_name,
anno_scale,
annotation_scale_handle,
true,
bg_color,
active_viewport,
&fallback_depths,
);
&fallback_cache
}
};
let is_xref = document
.block_records
.get(&insert.block_name)
.map(|record| record.flags.is_xref || record.flags.is_xref_overlay)
.unwrap_or(false);
let mut wires = cache::block_cache::expand_insert(
cache,
insert,
owner,
style.color,
style.aci,
style.pattern_length,
style.pattern,
style.line_weight_px,
style.layer,
style.layer_aci,
style.layer_plottable,
selected,
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
bg_color,
anno_scale,
options.suppress_root_points,
)
.unwrap_or_default();
if options.apply_xclip {
if let Some(filter) = pick::xclip::insert_spatial_filter(document, insert) {
let transform = crate::scene::render_graph::insert_transform(document, insert);
let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform);
pick::xclip::clip_wires(&mut wires, &polygon);
for wire in &mut wires {
if let Some(mut instance) = wire.render_instance {
instance.source_id = cache.clip_source_id(
instance.source_id,
&polygon,
instance.translation,
);
wire.render_instance = Some(instance);
}
}
let frame_mode =
crate::scene::frame::mode(document, crate::scene::frame::FrameKind::Xclip);
if polygon.len() >= 3 {
let mut frame = pick::xclip::frame_wire(
&polygon,
owner.value().to_string(),
style.color,
selected,
style.line_weight_px,
);
frame.display_visible = frame_mode != 0;
frame.plot_visible = frame_mode == 1;
wires.push(frame);
}
}
}
ExpandedBlockObject {
wires,
style,
is_xref,
}
}
/// The section mark's viewing direction (paper-space, unit), derived entirely
/// from the decoded model-documentation view graph:
///
@@ -785,99 +951,32 @@ fn tessellate_entity_inner(
// Render non-annotative dimensions from their stored picture block.
// Rebuild geometry only when no usable block exists.
if let EntityType::Dimension(dim) = e {
let baked = Some(dim.base().block_name.trim())
.filter(|name| !name.is_empty())
.filter(|_| !crate::scene::annotative::is_annotative(document, e))
.and_then(|name| {
document
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(name))
})
.filter(|record| !record.entity_handles.is_empty());
if let Some(record) = baked {
// The block's contents are already in world space and its base
// point is the origin, so the instance that carries them is the
// identity — it exists only to reuse the graph's ByBlock / layer-0
// colour resolution and its handling of anything nested inside.
let mut insert = acadrust::entities::Insert::new(
record.name.clone(),
acadrust::types::Vector3::ZERO,
);
insert.common = dim.base().common.clone();
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
if matches!(e, EntityType::Dimension(_)) {
if let Some(owned) = crate::scene::render_graph::owned_block_insert(document, e)
.filter(|owned| owned.render_picture)
{
let mut wires = expand_block_object(
document,
None,
annotation_scale_handle,
true,
&depths,
);
let mut wires = Vec::new();
graph.walk_insert(
&insert,
&owned.insert,
h,
|sub, context| {
// Direct POINTs are dimension definition markers.
context.insert_path.len() != 1 || !matches!(sub, EntityType::Point(_))
sel,
active_viewport,
bg_color,
anno_scale,
annotation_scale_handle,
block_cache,
view_aabb,
world_per_pixel,
pslt_factor,
BlockObjectOptions {
suppress_root_points: owned.suppress_root_points,
..BlockObjectOptions::default()
},
|sub, context| {
let has_book_color = view::render::has_resolved_book_color(document, sub);
let color_byblock =
!has_book_color && sub.common().color == acadrust::types::Color::ByBlock;
let color_layer0 = !has_book_color
&& view::render::is_effective_layer_zero(&sub.common().layer)
&& sub.common().color == acadrust::types::Color::ByLayer;
let contrast_bg = convert::tessellate::text_contrast_background(sub, bg_color);
let sub_color = view::render::adapt_to_bg(
view::render::render_style_for_viewport(
document,
sub,
active_viewport,
)
.0,
contrast_bg,
);
let style = context.style_for(document, sub);
let resolved_color = view::render::adapt_to_bg(style.0, contrast_bg);
let mut placed = sub.clone();
placed.apply_transform(&context.transform);
let sub_wires = tessellate_entity(
document,
selected,
active_viewport,
bg_color,
anno_scale,
annotation_scale_handle,
&placed,
block_cache,
view_aabb,
world_per_pixel,
paper_space,
);
for mut wire in sub_wires {
// Every wire answers to the dimension, so a pick selects
// the dimension rather than one of its pieces.
wire.name = h.value().to_string();
if sel {
wire.selected = true;
wire.color = WireModel::SELECTED;
} else if (color_byblock || color_layer0) && wire.color == sub_color {
wire.color = resolved_color;
wire.aci = style.4;
}
wires.push(wire);
}
},
);
)
.wires;
if !wires.is_empty() {
let aabb = entity_aabb(e);
for wire in &mut wires {
// As below: only stroke / fill wires take the whole-dimension
// box. An SDF-text wire keeps its own tight glyph box so the
// text pick area hugs the text.
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
set_wire_aabb(wire, aabb);
}
@@ -927,6 +1026,10 @@ fn tessellate_entity_inner(
entity_color,
line_weight_px,
anno_scale,
active_viewport,
annotation_scale_handle,
block_cache,
view_aabb,
world_per_pixel,
bg_color,
);
@@ -951,92 +1054,31 @@ fn tessellate_entity_inner(
// When the block exists we render it directly. Same pattern as
// Dimension's `block_name`.
if let EntityType::Table(table) = e {
if let Some(record) = table.block_record_handle.and_then(|handle| {
document
.block_records
.iter()
.find(|record| record.handle == handle)
}) {
if !record.entity_handles.is_empty() {
let mut insert =
acadrust::entities::Insert::new(
record.name.clone(),
table.insertion_point,
);
insert.rotation = table
.horizontal_direction
.y
.atan2(table.horizontal_direction.x);
insert.common = table.common.clone();
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
document,
None,
annotation_scale_handle,
true,
&depths,
);
let mut wires = Vec::new();
graph.walk_insert(
&insert,
h,
|_, _| true,
|sub, context| {
let has_book_color =
view::render::has_resolved_book_color(document, sub);
let color_byblock = !has_book_color
&& sub.common().color
== acadrust::types::Color::ByBlock;
let color_layer0 = !has_book_color
&& view::render::is_effective_layer_zero(
&sub.common().layer,
)
&& sub.common().color
== acadrust::types::Color::ByLayer;
let style = context.style_for(document, sub);
let mut placed = sub.clone();
placed.apply_transform(&context.transform);
let sub_wires = tessellate_entity(
document,
selected,
active_viewport,
bg_color,
anno_scale,
annotation_scale_handle,
&placed,
block_cache,
view_aabb,
world_per_pixel,
paper_space,
);
for mut wire in sub_wires {
wire.name = h.value().to_string();
if color_byblock {
wire.color = if sel {
WireModel::SELECTED
} else {
view::render::adapt_to_bg(style.0, bg_color)
};
wire.aci = style.4;
} else if color_layer0 && !sel {
wire.color =
view::render::adapt_to_bg(style.0, bg_color);
wire.aci = style.4;
}
wires.push(wire);
}
},
);
if !wires.is_empty() {
let aabb = entity_aabb(e);
for wire in &mut wires {
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
set_wire_aabb(wire, aabb);
}
if let Some(owned) = crate::scene::render_graph::owned_block_insert(document, e) {
let mut wires = expand_block_object(
document,
&owned.insert,
h,
sel,
active_viewport,
bg_color,
anno_scale,
annotation_scale_handle,
block_cache,
view_aabb,
world_per_pixel,
pslt_factor,
BlockObjectOptions::default(),
)
.wires;
if !wires.is_empty() {
let aabb = entity_aabb(e);
for wire in &mut wires {
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
set_wire_aabb(wire, aabb);
}
return wires;
}
return wires;
}
}
@@ -1079,34 +1121,6 @@ fn tessellate_entity_inner(
}
if let EntityType::Insert(ins) = e {
// Resolve the INSERT's own style so ByBlock sub-entities can inherit it.
let (ins_color, ins_pat_len, ins_pat, ins_lw_px, ins_aci) =
view::render::render_style_for_viewport(document, e, active_viewport);
let ins_color = view::render::adapt_to_bg(ins_color, bg_color);
// Resolve the INSERT's *layer* style — the layer-0 inheritance target
// for sub-entities on layer "0" with ByLayer properties (#221).
let ins_layer = {
let mut s = view::render::layer_render_style_viewport(
document,
&ins.common.layer,
active_viewport,
);
s.color = view::render::adapt_to_bg(s.color, bg_color);
s
};
let ins_layer_aci = document
.layers
.get(&ins.common.layer)
.and_then(|layer| match &layer.color {
acadrust::types::Color::Index(index) => Some(*index),
_ => None,
})
.unwrap_or(0);
let ins_layer_plottable = document
.layers
.get(&ins.common.layer)
.map(|layer| layer.is_plottable)
.unwrap_or(true);
let ip = glam::Vec3::new(
(ins.insert_point.x) as f32,
(ins.insert_point.y) as f32,
@@ -1146,83 +1160,27 @@ fn tessellate_entity_inner(
fill_tris_low: Vec::new(),
};
let fallback_depths = rustc_hash::FxHashMap::default();
let fallback_cache;
let cache = match block_cache {
Some(cache) if cache.defn(&ins.block_name).is_some() => cache,
_ => {
fallback_cache = cache::block_cache::BlockCache::build_for_block(
document,
&ins.block_name,
anno_scale,
annotation_scale_handle,
true,
bg_color,
active_viewport,
&fallback_depths,
);
&fallback_cache
}
};
let is_xref = document
.block_records
.get(&ins.block_name)
.map(|record| record.flags.is_xref || record.flags.is_xref_overlay)
.unwrap_or(false);
let mut wires = cache::block_cache::expand_insert(
cache,
let expanded = expand_block_object(
document,
ins,
h,
ins_color,
ins_aci,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
ins_layer_aci,
ins_layer_plottable,
sel,
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
active_viewport,
bg_color,
anno_scale,
)
.unwrap_or_default();
if let Some(filter) = pick::xclip::insert_spatial_filter(document, ins) {
let transform = crate::scene::render_graph::insert_transform(document, ins);
let polygon =
pick::xclip::world_clip_polygon_for_transform(filter, &transform);
pick::xclip::clip_wires(&mut wires, &polygon);
for wire in &mut wires {
if let Some(mut instance) = wire.render_instance {
instance.source_id = cache.clip_source_id(
instance.source_id,
&polygon,
instance.translation,
);
wire.render_instance = Some(instance);
}
}
let frame_mode = crate::scene::frame::mode(
document,
crate::scene::frame::FrameKind::Xclip,
);
if polygon.len() >= 3 {
let mut frame = pick::xclip::frame_wire(
&polygon,
h.value().to_string(),
ins_color,
sel,
ins_lw_px,
);
frame.display_visible = frame_mode != 0;
frame.plot_visible = frame_mode == 1;
wires.push(frame);
}
}
annotation_scale_handle,
block_cache,
view_aabb,
world_per_pixel,
pslt_factor,
BlockObjectOptions {
apply_xclip: true,
..BlockObjectOptions::default()
},
);
let style = expanded.style;
let is_xref = expanded.is_xref;
let mut wires = expanded.wires;
crate::entities::insert::append_insert_attribute_wires(
&mut wires,
@@ -1230,12 +1188,12 @@ fn tessellate_entity_inner(
ins,
h,
sel,
ins_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
ins_layer_plottable,
style.color,
style.pattern_length,
style.pattern,
style.line_weight_px,
style.layer,
style.layer_plottable,
bg_color,
is_xref,
pslt_factor,
+69 -103
View File
@@ -157,35 +157,6 @@ impl BlockStyle {
InsertStyleSpec::new(document, insert, viewport).resolve(parent)
}
pub fn for_owned(
document: &CadDocument,
entity: &EntityType,
parent: Option<Self>,
viewport: Option<Handle>,
) -> Self {
let on_layer0 = is_effective_layer_zero(&entity.common().layer);
let insert = parent
.map(|style| style.resolve(document, entity, viewport))
.unwrap_or_else(|| render_style_for_viewport(document, entity, viewport));
Self {
insert,
layer0: if on_layer0 {
parent
.map(|style| style.layer0)
.unwrap_or_else(|| layer_render_style_viewport(document, &entity.common().layer, viewport))
} else {
layer_render_style_viewport(document, &entity.common().layer, viewport)
},
layer0_aci: if on_layer0 {
parent
.map(|style| style.layer0_aci)
.unwrap_or_else(|| layer_aci(document, &entity.common().layer))
} else {
layer_aci(document, &entity.common().layer)
},
}
}
pub fn resolve(
self,
document: &CadDocument,
@@ -528,81 +499,16 @@ impl<'a> RenderSceneGraph<'a> {
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
{
match entity {
EntityType::Dimension(dimension) => {
let block_name = dimension.base().block_name.trim();
if block_name.is_empty()
|| stack
.iter()
.any(|name| name.eq_ignore_ascii_case(block_name))
{
return;
}
let placement =
Transform::from_translation(dimension.base().insertion_point)
.then(&context.transform);
let mut owned = context.clone();
owned.transform = placement;
if owned.root_handle.is_null() {
owned.root_handle = entity.common().handle;
}
owned.block_style = Some(BlockStyle::for_owned(
self.document,
entity,
context.block_style,
self.viewport,
));
owned.nesting_depth += 1;
stack.push(block_name.to_string());
self.walk_block(block_name, &owned, visible, leaf, stack);
stack.pop();
}
EntityType::Table(table) => {
let Some(record) = table.block_record_handle.and_then(|handle| {
self.document
.block_records
.iter()
.find(|record| record.handle == handle)
}) else {
return;
};
let mut insert = Insert::new(record.name.clone(), table.insertion_point);
insert.rotation = table
.horizontal_direction
.y
.atan2(table.horizontal_direction.x);
insert.common = table.common.clone();
self.walk_insert_instances(&insert, context, visible, leaf);
}
EntityType::MultiLeader(multileader)
if matches!(
multileader.content_type,
acadrust::entities::LeaderContentType::Block
) && multileader.context.has_block_contents =>
{
let Some(record) = multileader.block_content_handle.and_then(|handle| {
self.document
.block_records
.iter()
.find(|record| record.handle == handle)
}) else {
return;
};
let mut insert = Insert::new(
record.name.clone(),
multileader.context.block_content_location,
);
insert.common = multileader.common.clone();
insert.common.color = multileader.block_content_color.clone();
insert.set_x_scale(multileader.block_scale.x);
insert.set_y_scale(multileader.block_scale.y);
insert.set_z_scale(multileader.block_scale.z);
insert.rotation = multileader.block_rotation;
insert.normal = multileader.context.block_content_normal;
self.walk_insert_instances(&insert, context, visible, leaf);
}
_ => {}
let Some(owned) = owned_block_insert(self.document, entity) else {
return;
};
if stack
.iter()
.any(|name| name.eq_ignore_ascii_case(&owned.insert.block_name))
{
return;
}
self.walk_insert_instances(&owned.insert, context, visible, leaf);
}
fn document_visible(&self, entity: &EntityType) -> bool {
@@ -649,6 +555,66 @@ pub fn insert_transform(document: &CadDocument, insert: &Insert) -> Transform {
.then(&insert.get_transform())
}
pub struct OwnedBlockInsert {
pub insert: Insert,
pub suppress_root_points: bool,
pub render_picture: bool,
}
pub fn owned_block_insert(
document: &CadDocument,
entity: &EntityType,
) -> Option<OwnedBlockInsert> {
let (insert, suppress_root_points, render_picture) = match entity {
EntityType::Dimension(dimension) => {
let name = dimension.base().block_name.trim();
let record = document
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(name))?;
if record.entity_handles.is_empty() {
return None;
}
let mut insert = Insert::new(record.name.clone(), Vector3::ZERO);
insert.common = dimension.base().common.clone();
(
insert,
true,
!crate::scene::annotative::is_annotative(document, entity),
)
}
EntityType::Table(table) => {
let record = table.block_record_handle.and_then(|handle| {
document
.block_records
.iter()
.find(|record| record.handle == handle)
})?;
if record.entity_handles.is_empty() {
return None;
}
let mut insert = Insert::new(record.name.clone(), table.insertion_point);
insert.rotation = table
.horizontal_direction
.y
.atan2(table.horizontal_direction.x);
insert.common = table.common.clone();
(insert, false, true)
}
EntityType::MultiLeader(multileader) => (
crate::entities::multileader::block_content_insert(document, multileader)?,
false,
true,
),
_ => return None,
};
Some(OwnedBlockInsert {
insert,
suppress_root_points,
render_picture,
})
}
pub fn array_offsets(insert: &Insert) -> Vec<[f64; 3]> {
if !insert.is_minsert() {
return vec![[0.0; 3]];