Files
OneCAD-Draft/src/scene/render_graph.rs
T
Hakan Seven abb4f4bf56 fix(blocks): unify nested object transforms
Route block-backed entities through one local geometry and accumulated transform path.

Refs #864
2026-08-30 23:30:14 +03:00

702 lines
23 KiB
Rust

//! Shared render-scene traversal.
//!
//! The drawing database stays authoritative. This layer only resolves the
//! hierarchy and per-instance context needed by render backends: space roots,
//! nested block references, transforms, arrays, visibility, style inheritance,
//! draw order, and clip boundaries. Leaf entities remain responsible for
//! producing their normal wire, hatch, image, wipeout, or mesh model.
use acadrust::entities::Insert;
use acadrust::types::{Color, Transform, Vector3};
use acadrust::{CadDocument, EntityType, Handle};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::scene::view::render::{
has_resolved_book_color, is_effective_layer_zero, layer_render_style_viewport,
render_style_for_block_sub_viewport, render_style_for_viewport, InheritStyle,
};
pub type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8);
/// A block record used as a render root. Model space, paper space, and an
/// ordinary definition opened for editing share the same ownership mechanism;
/// only their runtime role differs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockRoot {
pub record: Handle,
pub role: BlockRootRole,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlockRootRole {
ModelSpace,
PaperSpace,
DefinitionEdit,
}
/// Semantic root of one render traversal. A viewport is a projection edge from
/// its paper-space owner to model-space content, not another storage container.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SceneRoot {
Block(BlockRoot),
Viewport {
paper_block: Handle,
viewport: Handle,
model_block: Handle,
},
}
impl SceneRoot {
pub fn content_block(self) -> Handle {
match self {
Self::Block(root) => root.record,
Self::Viewport { model_block, .. } => model_block,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct BlockStyle {
pub insert: ResolvedStyle,
pub layer0: InheritStyle,
pub layer0_aci: u8,
}
#[derive(Clone, Copy, Debug)]
pub struct InsertStyleSpec {
own: BlockStyle,
color_byblock: bool,
color_bylayer: bool,
linetype_byblock: bool,
linetype_bylayer: bool,
lineweight_byblock: bool,
lineweight_bylayer: bool,
layer0: bool,
}
impl InsertStyleSpec {
pub fn new(document: &CadDocument, insert: &Insert, viewport: Option<Handle>) -> Self {
let entity = EntityType::Insert(insert.clone());
let has_book_color = has_resolved_book_color(document, &entity);
let linetype = &insert.common.linetype;
Self {
own: BlockStyle::for_entity(document, &entity, viewport),
color_byblock: !has_book_color && insert.common.color == Color::ByBlock,
color_bylayer: !has_book_color && insert.common.color == Color::ByLayer,
linetype_byblock: linetype.eq_ignore_ascii_case("byblock"),
linetype_bylayer: linetype.is_empty() || linetype.eq_ignore_ascii_case("bylayer"),
lineweight_byblock: matches!(
insert.common.line_weight,
acadrust::types::LineWeight::ByBlock
),
lineweight_bylayer: matches!(
insert.common.line_weight,
acadrust::types::LineWeight::ByLayer
| acadrust::types::LineWeight::Default
),
layer0: is_effective_layer_zero(&insert.common.layer),
}
}
pub fn resolve(self, parent: BlockStyle) -> BlockStyle {
let mut insert = self.own.insert;
if self.color_byblock {
insert.0 = parent.insert.0;
insert.4 = parent.insert.4;
} else if self.layer0 && self.color_bylayer {
insert.0 = parent.layer0.color;
insert.4 = parent.layer0_aci;
}
if self.linetype_byblock {
insert.1 = parent.insert.1;
insert.2 = parent.insert.2;
} else if self.layer0 && self.linetype_bylayer {
insert.1 = parent.layer0.pat_len;
insert.2 = parent.layer0.pat;
}
if self.lineweight_byblock {
insert.3 = parent.insert.3;
} else if self.layer0 && self.lineweight_bylayer {
insert.3 = parent.layer0.lw_px;
}
BlockStyle {
insert,
layer0: if self.layer0 {
parent.layer0
} else {
self.own.layer0
},
layer0_aci: if self.layer0 {
parent.layer0_aci
} else {
self.own.layer0_aci
},
}
}
}
impl BlockStyle {
pub fn for_entity(
document: &CadDocument,
entity: &EntityType,
viewport: Option<Handle>,
) -> Self {
Self {
insert: render_style_for_viewport(document, entity, viewport),
layer0: layer_render_style_viewport(document, &entity.common().layer, viewport),
layer0_aci: layer_aci(document, &entity.common().layer),
}
}
pub fn for_nested(
document: &CadDocument,
insert: &Insert,
parent: Self,
viewport: Option<Handle>,
) -> Self {
InsertStyleSpec::new(document, insert, viewport).resolve(parent)
}
pub fn resolve(
self,
document: &CadDocument,
entity: &EntityType,
viewport: Option<Handle>,
) -> ResolvedStyle {
let mut resolved = render_style_for_block_sub_viewport(
document,
entity,
self.insert.0,
self.insert.1,
self.insert.2,
self.insert.3,
self.layer0,
viewport,
);
let common = entity.common();
let has_book_color = has_resolved_book_color(document, entity);
resolved.4 = if !has_book_color && common.color == Color::ByBlock {
self.insert.4
} else if !has_book_color
&& is_effective_layer_zero(&common.layer)
&& common.color == Color::ByLayer
{
self.layer0_aci
} else {
resolved.4
};
resolved
}
}
fn layer_aci(document: &CadDocument, layer: &str) -> u8 {
document
.layers
.get(layer)
.and_then(|layer| match &layer.color {
Color::Index(index) => Some(*index),
_ => None,
})
.unwrap_or(0)
}
#[derive(Clone, Debug)]
pub struct RenderContext {
pub transform: Transform,
pub root_handle: Handle,
pub parent_insert: Handle,
pub insert_path: Vec<Insert>,
pub clips: Vec<Vec<[f64; 2]>>,
pub block_style: Option<BlockStyle>,
pub depth_base: f32,
pub depth_scale: f32,
pub nesting_depth: usize,
pub viewport: Option<Handle>,
}
impl RenderContext {
fn direct(depth_base: f32, viewport: Option<Handle>) -> Self {
Self {
transform: Transform::identity(),
root_handle: Handle::NULL,
parent_insert: Handle::NULL,
insert_path: Vec::new(),
clips: Vec::new(),
block_style: None,
depth_base,
depth_scale: 1.0,
nesting_depth: 0,
viewport,
}
}
pub fn is_instanced(&self) -> bool {
!self.root_handle.is_null()
}
pub fn style_for(&self, document: &CadDocument, entity: &EntityType) -> ResolvedStyle {
self.block_style
.map(|style| style.resolve(document, entity, self.viewport))
.unwrap_or_else(|| render_style_for_viewport(document, entity, self.viewport))
}
pub fn draw_depth(&self, handle: Handle, depths: &FxHashMap<u64, [f32; 2]>) -> f32 {
if self.is_instanced() {
self.depth_base
+ depths
.get(&handle.value())
.map_or(0.0, |depth| depth[0])
* self.depth_scale
} else {
depths
.get(&handle.value())
.map_or(self.depth_base, |depth| depth[0])
}
}
}
pub struct RenderSceneGraph<'a> {
document: &'a CadDocument,
frozen_layers: Option<&'a FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
depths: &'a FxHashMap<u64, [f32; 2]>,
viewport: Option<Handle>,
}
impl<'a> RenderSceneGraph<'a> {
pub fn new(
document: &'a CadDocument,
frozen_layers: Option<&'a FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
depths: &'a FxHashMap<u64, [f32; 2]>,
) -> Self {
Self {
document,
frozen_layers,
annotation_scale_handle,
all_visible,
depths,
viewport: None,
}
}
pub fn with_viewport(mut self, viewport: Option<Handle>) -> Self {
self.viewport = viewport.filter(|handle| handle.is_valid());
self
}
/// Walk direct root entities and every referenced block subtree. `visible`
/// can add session-only rules such as isolate/preview hiding; returning
/// false for an Insert removes its whole subtree.
pub fn walk_root<V, F>(&self, root: SceneRoot, mut visible: V, mut leaf: F)
where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
{
let block = root.content_block();
let viewport = match root {
SceneRoot::Viewport { viewport, .. } => Some(viewport),
SceneRoot::Block(_) => self.viewport,
};
let Some(record) = self
.document
.block_records
.iter()
.find(|record| record.handle == block)
else {
return;
};
for &handle in &record.entity_handles {
let Some(source) = self.document.get_entity(handle) else {
continue;
};
let contextual = crate::scene::annotative::entity_for_annotation_context(
self.document,
source,
self.annotation_scale_handle,
);
let entity = contextual.as_ref();
let direct_depth = self
.depths
.get(&handle.value())
.map_or(0.0, |depth| depth[0]);
let context = RenderContext::direct(direct_depth, viewport);
if !self.document_visible(entity) || !visible(entity, &context) {
continue;
}
if let EntityType::Insert(insert) = entity {
self.walk_insert_instances(insert, &context, &mut visible, &mut leaf);
} else {
leaf(entity, &context);
self.walk_owned_content(entity, &context, &mut visible, &mut leaf, &mut Vec::new());
}
}
}
/// Walk one synthetic or document-owned Insert. Used by entity renderers
/// whose content is itself a block reference.
pub fn walk_insert<V, F>(
&self,
insert: &Insert,
root_handle: Handle,
mut visible: V,
mut leaf: F,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
{
let mut root_insert = insert.clone();
root_insert.common.handle = root_handle;
let entity = EntityType::Insert(root_insert.clone());
let depth_base = self
.depths
.get(&root_handle.value())
.map_or(0.0, |depth| depth[0]);
let context = RenderContext::direct(depth_base, self.viewport);
if self.document_visible(&entity) && visible(&entity, &context) {
self.walk_insert_instances(&root_insert, &context, &mut visible, &mut leaf);
}
}
fn walk_insert_instances<V, F>(
&self,
insert: &Insert,
parent: &RenderContext,
visible: &mut V,
leaf: &mut F,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
{
let insert_entity = EntityType::Insert(insert.clone());
let block_style = parent
.block_style
.map(|style| BlockStyle::for_nested(self.document, insert, style, self.viewport))
.unwrap_or_else(|| BlockStyle::for_entity(self.document, &insert_entity, self.viewport));
let root_handle = if parent.root_handle.is_null() {
insert.common.handle
} else {
parent.root_handle
};
let [depth_base, depth_scale] = if parent.root_handle.is_null() {
self.depths
.get(&insert.common.handle.value())
.copied()
.unwrap_or([0.0, 1.0])
} else {
let base = parent.draw_depth(insert.common.handle, self.depths);
let count = self
.document
.block_records
.get(&insert.block_name)
.map_or(1, |record| record.entity_handles.len().max(1));
[base, parent.depth_scale / (count as f32 + 1.0)]
};
for offset in array_offsets(insert) {
let local = insert_instance_transform(self.document, insert, offset);
let transform = local.then(&parent.transform);
let mut context = parent.clone();
context.transform = transform;
context.root_handle = root_handle;
context.parent_insert = insert.common.handle;
context.insert_path.push(insert.clone());
context.block_style = Some(block_style);
context.depth_base = depth_base;
context.depth_scale = depth_scale;
context.nesting_depth += 1;
if let Some(filter) = crate::scene::pick::xclip::insert_spatial_filter(
self.document,
insert,
) {
let polygon = crate::scene::pick::xclip::world_clip_polygon_for_transform(
filter,
&transform,
);
if polygon.len() >= 3 {
context.clips.push(polygon);
}
}
let mut stack = context
.insert_path
.iter()
.map(|insert| insert.block_name.clone())
.collect();
self.walk_block(
&insert.block_name,
&context,
visible,
leaf,
&mut stack,
);
}
}
fn walk_block<V, F>(
&self,
block_name: &str,
context: &RenderContext,
visible: &mut V,
leaf: &mut F,
stack: &mut Vec<String>,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
{
if context.nesting_depth > 32 {
return;
}
let Some(record) = self.document.block_records.get(block_name) else {
return;
};
for &handle in &record.entity_handles {
let Some(source) = self.document.get_entity(handle) else {
continue;
};
let contextual = crate::scene::annotative::entity_for_annotation_context(
self.document,
source,
self.annotation_scale_handle,
);
let entity = contextual.as_ref();
if !self.document_visible(entity) || !visible(entity, context) {
continue;
}
match entity {
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_) => {}
EntityType::Insert(nested) => {
if stack
.iter()
.any(|name| name.eq_ignore_ascii_case(&nested.block_name))
{
continue;
}
stack.push(nested.block_name.clone());
self.walk_insert_instances(nested, context, visible, leaf);
stack.pop();
}
_ => {
leaf(entity, context);
self.walk_owned_content(entity, context, visible, leaf, stack);
}
}
}
}
fn walk_owned_content<V, F>(
&self,
entity: &EntityType,
context: &RenderContext,
visible: &mut V,
leaf: &mut F,
stack: &mut Vec<String>,
) where
V: FnMut(&EntityType, &RenderContext) -> bool,
F: FnMut(&EntityType, &RenderContext),
{
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 {
let common = entity.common();
if common.invisible {
return false;
}
let layer = self.document.layers.get(&common.layer);
if layer
.map(|layer| layer.flags.off || layer.flags.frozen)
.unwrap_or(false)
{
return false;
}
if self.frozen_layers.is_some_and(|frozen| {
layer.is_some_and(|layer| frozen.contains(&layer.handle))
}) {
return false;
}
!crate::scene::annotative::annotative_offscale_for(
self.document,
common,
self.annotation_scale_handle,
self.all_visible,
)
}
}
pub fn block_base_point(document: &CadDocument, block_name: &str) -> Vector3 {
document
.block_records
.get(block_name)
.and_then(|record| document.get_entity(record.block_entity_handle))
.and_then(|entity| match entity {
EntityType::Block(block) => Some(block.base_point),
_ => None,
})
.unwrap_or(Vector3::ZERO)
}
pub fn insert_transform(document: &CadDocument, insert: &Insert) -> Transform {
let base = block_base_point(document, &insert.block_name);
Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z))
.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]];
}
let mut offsets = Vec::with_capacity(insert.instance_count());
for row in 0..insert.row_count {
for column in 0..insert.column_count {
offsets.push([
column as f64 * insert.column_spacing,
row as f64 * insert.row_spacing,
0.0,
]);
}
}
offsets
}
pub fn insert_instance_transform(
document: &CadDocument,
insert: &Insert,
offset: [f64; 3],
) -> Transform {
let transform = insert_transform(document, insert);
if offset == [0.0; 3] {
transform
} else {
Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2]))
.then(&transform)
}
}
pub fn block_contains_hatch(
document: &CadDocument,
block_name: &str,
memo: &mut std::collections::HashMap<String, bool>,
) -> bool {
if let Some(&contains) = memo.get(block_name) {
return contains;
}
memo.insert(block_name.to_string(), false);
let contains = document
.block_records
.get(block_name)
.is_some_and(|record| {
record
.entity_handles
.iter()
.any(|&handle| match document.get_entity(handle) {
Some(EntityType::Hatch(_)) => true,
Some(EntityType::Insert(insert)) => {
block_contains_hatch(document, &insert.block_name, memo)
}
Some(EntityType::Dimension(dimension)) => {
let name = dimension.base().block_name.trim();
!name.is_empty() && block_contains_hatch(document, name, memo)
}
Some(EntityType::Table(table)) => table
.block_record_handle
.and_then(|handle| {
document
.block_records
.iter()
.find(|record| record.handle == handle)
})
.is_some_and(|record| {
block_contains_hatch(document, &record.name, memo)
}),
Some(EntityType::MultiLeader(multileader)) => multileader
.block_content_handle
.and_then(|handle| {
document
.block_records
.iter()
.find(|record| record.handle == handle)
})
.is_some_and(|record| {
block_contains_hatch(document, &record.name, memo)
}),
_ => false,
})
});
memo.insert(block_name.to_string(), contains);
contains
}