refactor(render): unify scene rendering paths

Route model, paper, viewport and nested block content through one traversal while preserving leaf renderers. Carry decoded material state into mesh shading and harden solid geometry conversion.
This commit is contained in:
Hakan Seven 2026-08-05 21:44:59 +03:00
commit b667eeeb05
17 changed files with 2218 additions and 1871 deletions

View file

@ -29,14 +29,19 @@ fn material_map_text(map: &acadrust::objects::MaterialMap) -> String {
} }
} }
2 => format!( 2 => format!(
"Procedural {}", "Procedural {:#?}",
map.texture.as_ref().map_or(0, |texture| texture.mode) map.texture
), ),
_ => "None".to_string(), 0 => "Scene".to_string(),
value => format!("Source {value}"),
}; };
format!( format!(
"{source}; blend {:.3}; projection {}; tiling {}; auto {}", "{source}; blend {:.3}; projection {}; tiling {}; auto {}; transform {:?}",
map.blend_factor, map.projection, map.tiling, map.auto_transform map.blend_factor,
map.projection,
map.tiling,
map.auto_transform,
map.transform
) )
} }
@ -568,11 +573,31 @@ impl OpenCADStudio {
t!("Advanced").as_ref(), t!("Advanced").as_ref(),
"mat_advanced", "mat_advanced",
format!( format!(
"normal {:.3}; bump {:.3}; reflect {:.3}; transmit {:.3}", "normal method {}; strength {:.3}; bump {:.3}; reflect {:.3}; transmit {:.3}; bleed {:.3}",
material.normal_map_method,
material.normal_map_strength, material.normal_map_strength,
material.indirect_bump_scale, material.indirect_bump_scale,
material.reflectance_scale, material.reflectance_scale,
material.transmittance_scale material.transmittance_scale,
material.color_bleed_scale
),
),
ro_prop(
t!("Indirect Lighting").as_ref(),
"mat_indirect_lighting",
format!(
"global {}; final gather {}",
material.global_illumination,
material.final_gather
),
),
ro_prop(
t!("Source State").as_ref(),
"mat_source_state",
format!(
"advanced {}; anonymous {}",
material.advanced_data_present,
material.is_anonymous
), ),
), ),
], ],

View file

@ -1309,7 +1309,8 @@ use acadrust::{CadDocument, EntityType, Handle};
use crate::scene::convert::tess_util::aci_to_rgba; use crate::scene::convert::tess_util::aci_to_rgba;
use crate::scene::convert::tessellate::{ use crate::scene::convert::tessellate::{
add_polyline, add_segment, append_arrow, arrow_from_block, normalized_or, ArrowKind, DimGeom, add_polyline, add_segment, append_arrow, arrow_from_block_with_deferred_hatch,
normalized_or, ArrowKind, DimGeom,
}; };
use crate::scene::model::wire_model::{SnapHint, WireModel}; use crate::scene::model::wire_model::{SnapHint, WireModel};
@ -1660,6 +1661,16 @@ fn tessellate_dimension_inner(
// 3. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end). // 3. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end).
// Unknown / NULL block handles fall back to ClosedFilled. // Unknown / NULL block handles fall back to ClosedFilled.
let dimasz = (dimasz_raw as f32).max(0.001); let dimasz = (dimasz_raw as f32).max(0.001);
let defer_arrow_hatches = {
let name = dim.base().block_name.trim();
let mut memo = std::collections::HashMap::new();
!name.is_empty()
&& crate::scene::render_graph::block_contains_hatch(
document,
name,
&mut memo,
)
};
let (arrow1, arrow2) = if dimtsz_raw > 1e-9 { let (arrow1, arrow2) = if dimtsz_raw > 1e-9 {
let t = ArrowKind::Tick { let t = ArrowKind::Tick {
size: (dimtsz_raw as f32).max(0.001), size: (dimtsz_raw as f32).max(0.001),
@ -1668,11 +1679,26 @@ fn tessellate_dimension_inner(
} else if let Some(s) = style { } else if let Some(s) = style {
if dimsah { if dimsah {
( (
arrow_from_block(document, s.dimblk1, dimasz), arrow_from_block_with_deferred_hatch(
arrow_from_block(document, s.dimblk2, dimasz), document,
s.dimblk1,
dimasz,
defer_arrow_hatches,
),
arrow_from_block_with_deferred_hatch(
document,
s.dimblk2,
dimasz,
defer_arrow_hatches,
),
) )
} else { } else {
let a = arrow_from_block(document, s.dimblk, dimasz); let a = arrow_from_block_with_deferred_hatch(
document,
s.dimblk,
dimasz,
defer_arrow_hatches,
);
(a.clone(), a) (a.clone(), a)
} }
} else { } else {

View file

@ -1538,9 +1538,9 @@ impl MultiLeaderTess for MultiLeader {
// ── Block content ─────────────────────────────────────────────────────── // ── Block content ───────────────────────────────────────────────────────
// When content_type == Block, the MultiLeader displays a block reference // When content_type == Block, the MultiLeader displays a block reference
// at block_content_location with the recorded rotation/scale. Synthesize // at block_content_location with the recorded rotation/scale. A
// an Insert and explode it through the standard tessellator. The block // synthetic Insert supplies that context to the shared scene graph;
// resolves via block_content_handle (handle → block_record name). // every child then uses its normal tessellator.
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents { if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
let block_name = match ml.block_content_handle { let block_name = match ml.block_content_handle {
Some(h) if !h.is_null() => document Some(h) if !h.is_null() => document
@ -1567,29 +1567,42 @@ impl MultiLeaderTess for MultiLeader {
// anchor when *creating* the multileader; at render time the // anchor when *creating* the multileader; at render time the
// file's stored leader endpoints already encode that choice. // file's stored leader endpoints already encode that choice.
let _ = ml.block_connection_type; let _ = ml.block_connection_type;
for sub in synth_ins.explode_from_document(document) { let depths = rustc_hash::FxHashMap::default();
let normalized = let graph = crate::scene::render_graph::RenderSceneGraph::new(
crate::modules::draw::modify::explode::normalize_insert_entity(sub); document,
let mut sub_wires = tessellate( None,
document, None,
handle, true,
&normalized, &depths,
selected, );
block_color, graph.walk_insert(
leader_pat_len, &synth_ins,
leader_pat, handle,
leader_lw_px, |_, _| true,
1.0, |entity, context| {
None, let mut placed = entity.clone();
None, placed.apply_transform(&context.transform);
bg_color, let mut sub_wires = tessellate(
false, document,
); handle,
for w in &mut sub_wires { &placed,
w.name = name.clone(); selected,
} block_color,
wires.extend(sub_wires); 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 // Block attributes attached to the multileader — render each as
// its own attribute entity at WCS location like INSERT does. // its own attribute entity at WCS location like INSERT does.
for ba in &ml.block_attributes { for ba in &ml.block_attributes {

View file

@ -100,27 +100,7 @@ pub struct LocalWire {
pub struct NestedRef { pub struct NestedRef {
pub block_name: String, pub block_name: String,
pub xform: Transform, pub xform: Transform,
/// Nested INSERT's own resolved style (used when child wires need pub style: crate::scene::render_graph::InsertStyleSpec,
/// to inherit something via ByBlock).
pub ins_color: [f32; 4],
pub ins_pat_len: f32,
pub ins_pat: [f32; 8],
pub ins_lw_px: f32,
pub color_is_byblock: bool,
pub lt_is_byblock: bool,
pub lw_is_byblock: bool,
/// The nested INSERT's own properties are ByLayer. Combined with
/// `layer_is_zero` these drive the layer-0 rule for the nested insert
/// *itself* (so its ByBlock leaves inherit the outer layer, not layer 0).
pub color_is_bylayer: bool,
pub lt_is_bylayer: bool,
pub lw_is_bylayer: bool,
/// The nested INSERT itself sits on layer "0": its own layer-0 children
/// chain up to the outer insert's layer rather than resolving to `l0`.
pub layer_is_zero: bool,
/// The nested INSERT's own layer style — the layer-0 inheritance target
/// for its children when the nested insert is not itself on layer 0.
pub l0: crate::scene::view::render::InheritStyle,
pub instance_offsets: Vec<[f64; 3]>, pub instance_offsets: Vec<[f64; 3]>,
/// XCLIP boundary for this nested insert, in the parent defn's local frame /// XCLIP boundary for this nested insert, in the parent defn's local frame
/// (`None` = unclipped). Baked at build time because the clip's spatial /// (`None` = unclipped). Baked at build time because the clip's spatial
@ -142,6 +122,7 @@ pub enum LocalSub {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct BlockDefn { pub struct BlockDefn {
pub subs: Vec<LocalSub>, pub subs: Vec<LocalSub>,
pub base_point: Vector3,
/// 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. Expressed in this defn's *offset* /// the wire renderer is 2D-dominant. Expressed in this defn's *offset*
@ -149,8 +130,8 @@ pub struct BlockDefn {
pub aabb_local: [f32; 4], pub aabb_local: [f32; 4],
/// Raw entity count of the source block record (`entity_handles.len()`). /// Raw entity count of the source block record (`entity_handles.len()`).
/// Divisor for nested depth composition: a nested insert's children get a /// Divisor for nested depth composition: a nested insert's children get a
/// sub-range of `parent_scale / (child_count + 1)` — the same formula the /// sub-range of `parent_scale / (child_count + 1)`, shared with the scene
/// exploded-fill path uses, so bands and fills stay on one depth scale. /// graph so bands and fills stay on one depth scale.
pub child_count: usize, pub child_count: usize,
} }
@ -203,8 +184,7 @@ impl BlockCache {
all_visible: bool, all_visible: bool,
bg_color: [f32; 4], bg_color: [f32; 4],
// Scene draw-depth map ([depth, half] per handle) — source of each // Scene draw-depth map ([depth, half] per handle) — source of each
// block child's in-block rank, so band depth composition agrees with // block child's in-block rank, shared by cached wires and graph leaves.
// the SortEntitiesTable-aware ranking the fill explosion uses.
depth_map: &HashMap<u64, [f32; 2]>, depth_map: &HashMap<u64, [f32; 2]>,
) -> Self { ) -> Self {
use crate::par::prelude::*; use crate::par::prelude::*;
@ -249,6 +229,41 @@ impl BlockCache {
cache cache
} }
/// Build a small cache rooted at one block. Used only when a synthetic or
/// newly-added Insert is not present in the resident cache yet.
pub fn build_for_block(
doc: &CadDocument,
block_name: &str,
anno_scale: f32,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
) -> Self {
use crate::par::prelude::*;
let mut cache = Self::new();
let referenced = referenced_block_tree(doc, block_name);
cache.defns = referenced
.par_iter()
.map(|name| {
(
name.clone(),
Arc::new(build_defn(
doc,
name,
anno_scale,
annotation_scale_handle,
all_visible,
bg_color,
depth_map,
)),
)
})
.collect();
cache.compute_block_aabbs(&referenced);
cache
}
/// Compute and store the `aabb_local` for every cached defn. Direct wires /// Compute and store the `aabb_local` for every cached defn. Direct wires
/// contribute their own aabb_local; nested INSERT references look up the /// contribute their own aabb_local; nested INSERT references look up the
/// nested defn (already cached) and transform its aabb_local by the /// nested defn (already cached) and transform its aabb_local by the
@ -346,6 +361,26 @@ fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
seen.into_iter().collect() seen.into_iter().collect()
} }
fn referenced_block_tree(doc: &CadDocument, root: &str) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::default();
let mut queue = vec![root.to_string()];
seen.insert(root.to_string());
while let Some(name) = queue.pop() {
let Some(record) = doc.block_records.get(&name) else {
continue;
};
for &handle in &record.entity_handles {
let Some(EntityType::Insert(insert)) = doc.get_entity(handle) else {
continue;
};
if seen.insert(insert.block_name.clone()) {
queue.push(insert.block_name.clone());
}
}
}
seen.into_iter().collect()
}
/// True when `layer` is turned off or frozen — entities on it never render. /// True when `layer` is turned off or frozen — entities on it never render.
fn layer_hidden(doc: &CadDocument, layer: &str) -> bool { fn layer_hidden(doc: &CadDocument, layer: &str) -> bool {
doc.layers doc.layers
@ -441,79 +476,67 @@ fn build_defn(
subs.push(LocalSub::Wire(wire)); subs.push(LocalSub::Wire(wire));
} }
} }
// A table nested in a block bakes its geometry (gridlines, cell // A nested table's stored graphics use the same scene-graph
// text, fills) into a `*T` block, laid out in block-LOCAL space with // traversal as top-level table content. The resulting leaf wires
// the table carrying the world placement in insertion_point + // remain local to this cached definition; the parent instance
// horizontal_direction (like an INSERT). The top-level path expands // transform is composed later by the cache expansion.
// and places it in `tessellate_entity`; the block-expand path skips EntityType::Table(table) => {
// that, so a nested table drew nothing. Place each `*T` sub by the let baked = table.block_record_handle.and_then(|handle| {
// table's rotation+insertion here (defn-local), then tessellate — the doc.block_records
// parent insert transform composes on top at expand time. .iter()
EntityType::Table(tab) => { .find(|record| record.handle == handle)
let tblk = tab });
.block_record_handle if let Some(record) = baked.filter(|record| {
.and_then(|h| doc.block_records.iter().find(|br| br.handle == h)); !record.entity_handles.is_empty()
let mut used_baked = false; }) {
if let Some(tblk) = tblk { let mut insert = acadrust::entities::Insert::new(
used_baked = !tblk.entity_handles.is_empty(); record.name.clone(),
let ins = tab.insertion_point; table.insertion_point,
let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x); );
for &teh in &tblk.entity_handles { insert.rotation = table
let Some(tsub) = doc.get_entity(teh) else { .horizontal_direction
continue; .y
}; .atan2(table.horizontal_direction.x);
if tsub.common().invisible || layer_hidden(doc, &tsub.common().layer) { insert.common = table.common.clone();
continue; let graph = crate::scene::render_graph::RenderSceneGraph::new(
} doc,
let mut placed = tsub.clone(); None,
{ annotation_scale_handle,
let ent = placed.as_entity_mut(); all_visible,
if angle.abs() > 1e-9 { depth_map,
ent.apply_rotation( );
acadrust::types::Vector3::new(0.0, 0.0, 1.0), graph.walk_insert(
angle, &insert,
); table.common.handle,
|_, _| true,
|leaf, context| {
let mut placed = leaf.clone();
placed.apply_transform(&context.transform);
for wire in tessellate_sub_local(
doc,
&placed,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
) {
subs.push(LocalSub::Wire(wire));
} }
ent.translate(ins); },
} );
// A baked table can nest inserts too (block cells); } else {
// route those through the nested-ref path like the for wire in tessellate_sub_local(
// dimension arrows above. doc,
if let EntityType::Insert(cell_ins) = &placed { entity,
subs.push(LocalSub::Nested(build_nested_ref( anno_scale,
cell_ins, doc, bg_color, depth_map, annotation_scale_handle,
))); bg_color,
} else { depth_map,
for lw in ) {
tessellate_sub_local( subs.push(LocalSub::Wire(wire));
doc,
&placed,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
)
{
subs.push(LocalSub::Wire(lw));
}
}
}
}
if !used_baked {
for lw in
tessellate_sub_local(
doc,
entity,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
)
{
subs.push(LocalSub::Wire(lw));
} }
for insert in crate::entities::table::block_cell_inserts( for insert in crate::entities::table::block_cell_inserts(
tab, table,
doc, doc,
anno_scale, anno_scale,
) { ) {
@ -546,6 +569,7 @@ fn build_defn(
} }
BlockDefn { BlockDefn {
subs, subs,
base_point: crate::scene::render_graph::block_base_point(doc, block_name),
aabb_local: [0.0; 4], aabb_local: [0.0; 4],
child_count: br.entity_handles.len(), child_count: br.entity_handles.len(),
} }
@ -557,49 +581,22 @@ fn build_nested_ref(
bg_color: [f32; 4], bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>, depth_map: &HashMap<u64, [f32; 2]>,
) -> NestedRef { ) -> NestedRef {
// Store the RAW colour — `adapt_to_bg` runs at emit time
// (`Batches::finalize`) so the same cached defn can serve renders
// against different backgrounds without rebuilding.
let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) =
crate::scene::view::render::render_style_for(doc, &EntityType::Insert(nested_ins.clone()));
// The nested insert's own layer style — the layer-0 target for its
// children (raw colour; adapted at emit like `ins_color`).
let l0 = crate::scene::view::render::layer_render_style(doc, &nested_ins.common.layer);
let _ = bg_color; let _ = bg_color;
let nested_entity = EntityType::Insert(nested_ins.clone());
let has_book_color =
crate::scene::view::render::has_resolved_book_color(doc, &nested_entity);
// Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps // Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps
// its clip when the parent block is expanded — the spatial filter object // its clip when the parent block is expanded — the spatial filter object
// isn't reachable at expand time. // isn't reachable at expand time.
let xform = crate::scene::render_graph::insert_transform(doc, nested_ins);
let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins) let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins)
.map(|sf| crate::scene::pick::xclip::world_clip_polygon_f64(sf, nested_ins)); .map(|filter| {
crate::scene::pick::xclip::world_clip_polygon_for_transform(filter, &xform)
});
NestedRef { NestedRef {
block_name: nested_ins.block_name.clone(), block_name: nested_ins.block_name.clone(),
xform: nested_ins.get_transform(), xform,
ins_color, style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins),
ins_pat_len, instance_offsets: crate::scene::render_graph::array_offsets(nested_ins),
ins_pat,
ins_lw_px,
color_is_byblock: !has_book_color && nested_ins.common.color == AcadColor::ByBlock,
lt_is_byblock: nested_ins.common.linetype.eq_ignore_ascii_case("byblock"),
lw_is_byblock: matches!(nested_ins.common.line_weight, LineWeight::ByBlock),
color_is_bylayer: !has_book_color && nested_ins.common.color == AcadColor::ByLayer,
lt_is_bylayer: {
let lt = &nested_ins.common.linetype;
lt.is_empty() || lt.eq_ignore_ascii_case("bylayer")
},
lw_is_bylayer: matches!(
nested_ins.common.line_weight,
LineWeight::ByLayer | LineWeight::Default
),
layer_is_zero: crate::scene::view::render::is_effective_layer_zero(
&nested_ins.common.layer,
),
l0,
instance_offsets: array_offsets(nested_ins),
clip_poly, clip_poly,
local_rank: depth_map local_rank: depth_map
.get(&nested_ins.common.handle.value()) .get(&nested_ins.common.handle.value())
@ -877,7 +874,9 @@ pub fn expand_insert(
anno_scale: f32, anno_scale: f32,
) -> Option<Vec<WireModel>> { ) -> Option<Vec<WireModel>> {
let defn = cache.defn(&ins.block_name)?; let defn = cache.defn(&ins.block_name)?;
let mut xform = ins.get_transform(); let base = defn.base_point;
let mut xform = Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z))
.then(&ins.get_transform());
// Annotative blocks (the flag lives on the block definition; the instance is // Annotative blocks (the flag lives on the block definition; the instance is
// marked with the AcAnnotativeData XDATA) scale as ONE uniform unit about // marked with the AcAnnotativeData XDATA) scale as ONE uniform unit about
// their insertion point — internal geometry/text/attributes are carried by // their insertion point — internal geometry/text/attributes are carried by
@ -979,7 +978,7 @@ pub fn expand_insert(
} }
} }
for offset in &array_offsets(ins) { for offset in &crate::scene::render_graph::array_offsets(ins) {
let base_xform = if offset == &[0.0; 3] { let base_xform = if offset == &[0.0; 3] {
xform.clone() xform.clone()
} else { } else {
@ -1475,38 +1474,25 @@ fn expand_defn(
// itself on layer "0" with ByLayer props inherits the outer // itself on layer "0" with ByLayer props inherits the outer
// layer-0 target (so its ByBlock leaves resolve to that layer, // layer-0 target (so its ByBlock leaves resolve to that layer,
// not layer 0). Mirrors the leaf resolution in emit_wire. // not layer 0). Mirrors the leaf resolution in emit_wire.
let nested_color = if nref.color_is_byblock { let parent_style = crate::scene::render_graph::BlockStyle {
ctx.ins_color insert: (
} else if nref.layer_is_zero && nref.color_is_bylayer { ctx.ins_color,
ctx.l0.color ctx.ins_pat_len,
} else { ctx.ins_pat,
nref.ins_color ctx.ins_lw_px,
0,
),
layer0: ctx.l0,
layer0_aci: 0,
}; };
let (nested_pat_len, nested_pat) = if nref.lt_is_byblock { let nested_style = nref.style.resolve(parent_style);
(ctx.ins_pat_len, ctx.ins_pat)
} else if nref.layer_is_zero && nref.lt_is_bylayer {
(ctx.l0.pat_len, ctx.l0.pat)
} else {
(nref.ins_pat_len, nref.ins_pat)
};
let nested_lw_px = if nref.lw_is_byblock {
ctx.ins_lw_px
} else if nref.layer_is_zero && nref.lw_is_bylayer {
ctx.l0.lw_px
} else {
nref.ins_lw_px
};
// Layer-0 target for the nested expansion: a nested insert that
// is itself on layer 0 chains up to the outer target; otherwise
// its layer-0 children resolve to the nested insert's own layer.
let nested_l0 = if nref.layer_is_zero { ctx.l0 } else { nref.l0 };
let inner_ctx = ExpandCtx { let inner_ctx = ExpandCtx {
cache: ctx.cache, cache: ctx.cache,
ins_color: nested_color, ins_color: nested_style.insert.0,
ins_pat_len: nested_pat_len, ins_pat_len: nested_style.insert.1,
ins_pat: nested_pat, ins_pat: nested_style.insert.2,
ins_lw_px: nested_lw_px, ins_lw_px: nested_style.insert.3,
l0: nested_l0, l0: nested_style.layer0,
selected: ctx.selected, selected: ctx.selected,
pslt_factor: ctx.pslt_factor, pslt_factor: ctx.pslt_factor,
view_aabb: ctx.view_aabb, view_aabb: ctx.view_aabb,
@ -1516,7 +1502,7 @@ fn expand_defn(
}; };
visited.push(nref.block_name.clone()); visited.push(nref.block_name.clone());
// Children of this nested insert stack inside the slot its own // Children of this nested insert stack inside the slot its own
// rank owns — same composition the exploded-fill path applies. // rank owns — same composition the scene graph applies.
let nested_range = ( let nested_range = (
d_range.0 + nref.local_rank * d_range.1, d_range.0 + nref.local_rank * d_range.1,
d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0), d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0),
@ -1985,20 +1971,3 @@ fn is_unreasonable_extent(e: &EntityType) -> bool {
_ => false, _ => false,
} }
} }
fn array_offsets(ins: &acadrust::entities::Insert) -> Vec<[f64; 3]> {
if !ins.is_minsert() {
return vec![[0.0; 3]];
}
let mut offsets = Vec::with_capacity(ins.instance_count());
for row in 0..ins.row_count {
for col in 0..ins.column_count {
offsets.push([
col as f64 * ins.column_spacing,
row as f64 * ins.row_spacing,
0.0,
]);
}
}
offsets
}

View file

@ -4,8 +4,8 @@
// • plane-surface faces → collect coedge-loop polygon, fan-triangulate. // • plane-surface faces → collect coedge-loop polygon, fan-triangulate.
// • cone-surface faces → sample a parametric grid (handles both cylinders // • cone-surface faces → sample a parametric grid (handles both cylinders
// and true cones). // and true cones).
// • sphere-surface faces → sample a full UV grid. // • sphere-surface faces → sample a boundary-clipped UV grid.
// • torus-surface faces → sample a full UV grid. // • torus-surface faces → sample a boundary-clipped UV grid.
// //
// Coverage is tracked explicitly. Unsupported or malformed faces retain // Coverage is tracked explicitly. Unsupported or malformed faces retain
// feature/display wires, and partial shells are marked non-complete so solid // feature/display wires, and partial shells are marked non-complete so solid
@ -552,16 +552,21 @@ fn torus_isolines(sat: &SatDocument, face: &SatFace, count: usize, out: &mut Vec
let v = cross3(axis, u); let v = cross3(axis, u);
let major = torus.major_radius(); let major = torus.major_radius();
let minor = torus.minor_radius(); let minor = torus.minor_radius();
let (phi_min, phi_span, full) = torus_phi_range(sat, face, [cx, cy, cz], u, v); let (phi_min, phi_span, full) =
torus_phi_range(sat, face, [cx, cy, cz], u, v, major);
let phi_total = if full { TAU } else { phi_span }; let phi_total = if full { TAU } else { phi_span };
let (theta_min, theta_span, theta_full) =
torus_theta_range(sat, face, [cx, cy, cz], axis, major, minor);
let theta_total = if theta_full { TAU } else { theta_span };
// Minor (cross-section) circles — constant revolution angle, full tube. // Minor (cross-section) arcs — constant revolution angle, clipped to the
// face's tube-angle window.
// Segment count from the tube (minor) radius at the shared edge tolerance. // Segment count from the tube (minor) radius at the shared edge tolerance.
let m = edge_arc_segs(minor, TAU); let m = edge_arc_segs(minor, theta_total);
for phi in iso_params(phi_min, phi_span, full, count) { for phi in iso_params(phi_min, phi_span, full, count) {
for t in 0..m { for t in 0..m {
let t0 = TAU * (t as f64 / m as f64); let t0 = theta_min + theta_total * (t as f64 / m as f64);
let t1 = TAU * ((t + 1) as f64 / m as f64); let t1 = theta_min + theta_total * ((t + 1) as f64 / m as f64);
out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t0, phi)); out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t0, phi));
out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t1, phi)); out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t1, phi));
} }
@ -573,8 +578,7 @@ fn torus_isolines(sat: &SatDocument, face: &SatFace, count: usize, out: &mut Vec
// cross-sections. `count.max(2)` guarantees outer + inner even at ISOLINES=1. // cross-sections. `count.max(2)` guarantees outer + inner even at ISOLINES=1.
let ring_segs = edge_arc_segs(major, phi_total).max(2); let ring_segs = edge_arc_segs(major, phi_total).max(2);
let n_ring = count.max(2); let n_ring = count.max(2);
for k in 0..n_ring { for theta in iso_params(theta_min, theta_span, theta_full, n_ring) {
let theta = TAU * (k as f64 / n_ring as f64);
for s in 0..ring_segs { for s in 0..ring_segs {
let p0 = phi_min + phi_total * (s as f64 / ring_segs as f64); let p0 = phi_min + phi_total * (s as f64 / ring_segs as f64);
let p1 = phi_min + phi_total * ((s + 1) as f64 / ring_segs as f64); let p1 = phi_min + phi_total * ((s + 1) as f64 / ring_segs as f64);
@ -622,54 +626,24 @@ fn sphere_param_range(
/// Revolution-angle arc a torus face spans, walking all its boundary loops. /// Revolution-angle arc a torus face spans, walking all its boundary loops.
/// ///
/// A partial tube ends in two minor-circle caps sitting in constant-φ planes; /// A partial tube ends in two minor-circle caps sitting in constant-φ planes.
/// the arc between them is the opening. But the tube body can also carry /// Their adjacent planar faces carry outward normals that identify which cap is
/// interior hole loops where another solid punches through it, so the widest /// the start and which is the end, so both short elbows and long open tubes use
/// empty gap is *not* reliably the opening — a hole splits the body into wide /// the correct side of the analytic surface. If that topology is absent or
/// hole-free stretches that masquerade as it. Only a gap flanked by two end /// ambiguous, the face remains untrimmed instead of guessing between the two
/// caps is genuinely surface-free, so that is the opening; the body is the rest /// possible arcs. Returns `(body_start, body_span, full)`.
/// of the turn. Returns `(body_start, body_span, full)`.
pub(crate) fn torus_phi_range( pub(crate) fn torus_phi_range(
sat: &SatDocument, sat: &SatDocument,
face: &SatFace, face: &SatFace,
center: [f64; 3], center: [f64; 3],
u: [f64; 3], u: [f64; 3],
v: [f64; 3], v: [f64; 3],
major: f64,
) -> (f64, f64, bool) { ) -> (f64, f64, bool) {
// Below this revolution-angle spread a loop is a minor circle sitting in a const GEOM_EPS: f64 = 1e-6;
// constant-φ plane — a tube end cap. Interference loops (where another solid let axis = norm3(cross3(u, v));
// punches through the tube) wander several degrees in φ, well above it. let mut starts = Vec::new();
const CAP_SPREAD: f64 = 0.15; // rad (~8.6°) let mut ends = Vec::new();
// Ignore sub-degree gaps *within* a cap's own point cluster; a genuine tube
// opening is far wider.
const MIN_OPENING: f64 = 0.02; // rad (~1.1°)
let phi_of = |p: [f64; 3]| -> f64 {
let rel = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
dot3(rel, v).atan2(dot3(rel, u)).rem_euclid(TAU)
};
// Angular spread of a set of φ values, wrap-aware: the turn minus the widest
// gap between them. A cap collapses to ~0; a hole loop keeps its real width.
let spread = |phis: &[f64]| -> f64 {
if phis.len() < 2 {
return 0.0;
}
let mut s = phis.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut gmax = 0.0f64;
for i in 0..s.len() {
let next = if i + 1 < s.len() {
s[i + 1]
} else {
s[0] + TAU
};
gmax = gmax.max(next - s[i]);
}
TAU - gmax
};
// Every boundary point, tagged by whether its loop is a tube end cap.
let mut tagged: Vec<(f64, bool)> = Vec::new();
let mut lp = face.first_loop(); let mut lp = face.first_loop();
let mut seen: HashSet<i32> = HashSet::default(); let mut seen: HashSet<i32> = HashSet::default();
while !lp.is_null() && seen.insert(lp.0) { while !lp.is_null() && seen.insert(lp.0) {
@ -677,46 +651,215 @@ pub(crate) fn torus_phi_range(
let Some(sl) = SatLoop::from_record(lr) else { let Some(sl) = SatLoop::from_record(lr) else {
break; break;
}; };
let poly = collect_loop_polygon(sat, &sl, BOUNDARY_CHORD_FRAC);
lp = sl.next_loop(); lp = sl.next_loop();
if poly.is_empty() { let Some((cap_center, cap_normal)) = adjacent_planar_circle_cap(sat, &sl) else {
continue;
};
let rel = [
cap_center[0] - center[0],
cap_center[1] - center[1],
cap_center[2] - center[2],
];
let axial = dot3(rel, axis);
let radial_u = dot3(rel, u);
let radial_v = dot3(rel, v);
let radial_len = (radial_u * radial_u + radial_v * radial_v).sqrt();
let scale = major.abs().max(radial_len).max(1.0);
if axial.abs() > scale * GEOM_EPS
|| (radial_len - major.abs()).abs() > scale * GEOM_EPS
{
continue; continue;
} }
let phis: Vec<f64> = poly.iter().map(|&p| phi_of(p)).collect(); let physical_phi = radial_v.atan2(radial_u);
let is_cap = spread(&phis) < CAP_SPREAD; let phi = if major < 0.0 {
for phi in phis { (physical_phi - std::f64::consts::PI).rem_euclid(TAU)
tagged.push((phi, is_cap));
}
}
if tagged.len() < 2 {
return (0.0, TAU, true);
}
tagged.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
// The opening is the arc with no surface: a gap flanked on *both* sides by
// end-cap points (a hole never bounds the opening). Of those, the tube's own
// opening is the narrowest above the intra-cluster noise floor; the body is
// the rest of the turn. No cap-cap gap ⇒ the tube closes into a full ring.
let n = tagged.len();
let mut opening_gap = f64::MAX;
let mut body_start = 0.0;
for i in 0..n {
let (phi_i, cap_i) = tagged[i];
let (phi_j, cap_j) = if i + 1 < n {
tagged[i + 1]
} else { } else {
(tagged[0].0 + TAU, tagged[0].1) physical_phi.rem_euclid(TAU)
}; };
let gap = phi_j - phi_i; let major_sign = if major < 0.0 { -1.0 } else { 1.0 };
if cap_i && cap_j && gap > MIN_OPENING && gap < opening_gap { let tangent = norm3([
opening_gap = gap; major_sign * (-u[0] * phi.sin() + v[0] * phi.cos()),
body_start = phi_j.rem_euclid(TAU); // body resumes past the far cap major_sign * (-u[1] * phi.sin() + v[1] * phi.cos()),
major_sign * (-u[2] * phi.sin() + v[2] * phi.cos()),
]);
let alignment = dot3(tangent, cap_normal);
if (alignment.abs() - 1.0).abs() > GEOM_EPS {
continue;
}
if alignment < 0.0 {
starts.push(phi);
} else {
ends.push(phi);
} }
} }
if opening_gap == f64::MAX {
starts.sort_by(|left, right| left.partial_cmp(right).unwrap());
ends.sort_by(|left, right| left.partial_cmp(right).unwrap());
starts.dedup_by(|left, right| (*left - *right).abs() < GEOM_EPS);
ends.dedup_by(|left, right| (*left - *right).abs() < GEOM_EPS);
if let ([start], [end]) = (starts.as_slice(), ends.as_slice()) {
let span = (end - start).rem_euclid(TAU);
if span > GEOM_EPS && span < TAU - GEOM_EPS {
return (*start, span, false);
}
}
(0.0, TAU, true)
}
/// Analytic circle data carried by an ellipse edge in a boundary loop.
fn loop_circle_geometry(
sat: &SatDocument,
sat_loop: &SatLoop,
) -> Option<([f64; 3], [f64; 3], f64)> {
let first = sat_loop.first_coedge();
let mut current = first;
let mut seen: HashSet<i32> = HashSet::default();
while !current.is_null() && seen.insert(current.0) {
let coedge = SatCoedge::from_record(sat.resolve(current)?)?;
if let Some(ellipse) = sat
.resolve(coedge.edge())
.and_then(SatEdge::from_record)
.and_then(|edge| sat.resolve(edge.curve()))
.and_then(SatEllipseCurve::from_record)
{
if (ellipse.ratio() - 1.0).abs() <= 1e-6 {
let (cx, cy, cz) = ellipse.center();
let (nx, ny, nz) = ellipse.normal();
let (mx, my, mz) = ellipse.major_axis();
let radius = (mx * mx + my * my + mz * mz).sqrt();
return Some(([cx, cy, cz], norm3([nx, ny, nz]), radius));
}
}
current = coedge.next();
if current == first {
break;
}
}
None
}
/// Circle centre and outward normal of the planar cap sharing a boundary loop
/// with a curved face. Both values come from analytic SAT records.
fn adjacent_planar_circle_cap(
sat: &SatDocument,
sat_loop: &SatLoop,
) -> Option<([f64; 3], [f64; 3])> {
let (circle_center, _, _) = loop_circle_geometry(sat, sat_loop)?;
let first = sat_loop.first_coedge();
let mut current = first;
let mut seen: HashSet<i32> = HashSet::default();
while !current.is_null() && seen.insert(current.0) {
let coedge = SatCoedge::from_record(sat.resolve(current)?)?;
if let Some(partner) = sat.resolve(coedge.partner()).and_then(SatCoedge::from_record) {
if let Some(partner_loop) = sat
.resolve(partner.owner_loop())
.and_then(SatLoop::from_record)
{
if let Some(adjacent_face) = sat
.resolve(partner_loop.face())
.and_then(SatFace::from_record)
{
if let Some(plane) = sat
.resolve(adjacent_face.surface())
.and_then(SatPlaneSurface::from_record)
{
let (nx, ny, nz) = plane.normal();
let normal = if matches!(adjacent_face.sense(), Sense::Reversed) {
[-nx, -ny, -nz]
} else {
[nx, ny, nz]
};
return Some((circle_center, norm3(normal)));
}
}
}
}
current = coedge.next();
if current == first {
break;
}
}
None
}
/// Tube-angle span of a torus face from its boundary loops.
///
/// Ring tori normally cover the complete minor circle, while a spindle-torus
/// cap terminates at the surface's axial singularity and owns just one
/// constant-θ boundary ring. Sampling the complete minor circle in that case
/// duplicates the cap across the rest of the self-intersecting analytic
/// surface, producing a ball much larger than the bounded face.
fn torus_theta_range(
sat: &SatDocument,
face: &SatFace,
center: [f64; 3],
axis: [f64; 3],
major: f64,
minor: f64,
) -> (f64, f64, bool) {
const GEOM_EPS: f64 = 1e-6;
if minor <= 1e-9 || major.abs() >= minor {
return (0.0, TAU, true); return (0.0, TAU, true);
} }
(body_start, TAU - opening_gap, false)
let mut rims = Vec::new();
let mut lp = face.first_loop();
let mut seen: HashSet<i32> = HashSet::default();
while !lp.is_null() && seen.insert(lp.0) {
let Some(sat_loop) = sat.resolve(lp).and_then(SatLoop::from_record) else {
break;
};
lp = sat_loop.next_loop();
let Some((circle_center, circle_normal, circle_radius)) =
loop_circle_geometry(sat, &sat_loop)
else {
continue;
};
let rel = [
circle_center[0] - center[0],
circle_center[1] - center[1],
circle_center[2] - center[2],
];
let axial = dot3(rel, axis);
let planar = [
rel[0] - axial * axis[0],
rel[1] - axial * axis[1],
rel[2] - axial * axis[2],
];
let scale = major.abs().max(minor).max(circle_radius).max(1.0);
if dot3(planar, planar).sqrt() > scale * GEOM_EPS
|| (dot3(circle_normal, axis).abs() - 1.0).abs() > GEOM_EPS
{
continue;
}
// The principal spindle branch has non-negative revolution radius:
// circle_radius = major + minor·cos(θ). Together with the circle
// plane's axial coordinate this recovers θ directly.
let sin_theta = axial / minor;
let cos_theta = (circle_radius - major) / minor;
if (sin_theta * sin_theta + cos_theta * cos_theta - 1.0).abs() > GEOM_EPS {
continue;
}
rims.push(sin_theta.atan2(cos_theta));
}
rims.sort_by(|left, right| left.partial_cmp(right).unwrap());
rims.dedup_by(|left, right| (*left - *right).abs() < GEOM_EPS);
if rims.len() != 1 || rims[0].abs() < GEOM_EPS {
return (0.0, TAU, true);
}
let rim = rims[0];
let singular = (-major / minor).clamp(-1.0, 1.0).acos();
if rim > 0.0 && rim < singular {
(rim, singular - rim, false)
} else if rim < 0.0 && rim > -singular {
(-singular, singular + rim, false)
} else {
(0.0, TAU, true)
}
} }
/// Reduce a set of angles to a `(min, span, full)` arc. Mirrors `angular_range`'s /// Reduce a set of angles to a `(min, span, full)` arc. Mirrors `angular_range`'s
@ -868,7 +1011,22 @@ fn collect_curved_gens(
torus.u_direction().2, torus.u_direction().2,
]); ]);
let v = cross3(axis, u); let v = cross3(axis, u);
let (pmin, pspan, full) = torus_phi_range(sat, &face, [cx, cy, cz], u, v); let (pmin, pspan, full) = torus_phi_range(
sat,
&face,
[cx, cy, cz],
u,
v,
torus.major_radius(),
);
let (tmin, tspan, tfull) = torus_theta_range(
sat,
&face,
[cx, cy, cz],
axis,
torus.major_radius(),
torus.minor_radius(),
);
let (center, center_low) = place([cx, cy, cz]); let (center, center_low) = place([cx, cy, cz]);
out.push(CurvedGen::Torus { out.push(CurvedGen::Torus {
center, center,
@ -881,6 +1039,9 @@ fn collect_curved_gens(
phi_min: pmin as f32, phi_min: pmin as f32,
phi_span: pspan as f32, phi_span: pspan as f32,
full, full,
theta_min: tmin as f32,
theta_span: tspan as f32,
theta_full: tfull,
}); });
} }
_ => {} _ => {}
@ -2093,14 +2254,19 @@ pub(crate) fn tess_torus_face(
let v_dir = cross3(axis, u_dir); let v_dir = cross3(axis, u_dir);
let major_r = torus.major_radius(); let major_r = torus.major_radius();
let minor_r = torus.minor_radius(); let minor_r = torus.minor_radius();
let (theta_start, theta_arc, theta_full) =
// Tube cross-section is a closed minor circle → full-circle segment count at torus_theta_range(sat, face, [cx, cy, cz], axis, major_r, minor_r);
// the tube (minor) radius. let theta_total = if theta_full { TAU } else { theta_arc };
let nu = lod.circle_segs(minor_r).max(3); // around the tube let nu = if theta_full {
lod.circle_segs(minor_r).max(3)
} else {
lod.arc_segs(minor_r, theta_total).max(1)
};
// Mesh only the revolution arc the face covers. A partial tube (an open "C") // Mesh only the revolution arc the face covers. A partial tube (an open "C")
// otherwise builds as a full closed ring where the solid is open. // otherwise builds as a full closed ring where the solid is open.
let (phi_start, phi_arc, full) = torus_phi_range(sat, face, [cx, cy, cz], u_dir, v_dir); let (phi_start, phi_arc, full) =
torus_phi_range(sat, face, [cx, cy, cz], u_dir, v_dir, major_r);
let phi_total = if full { TAU } else { phi_arc }; let phi_total = if full { TAU } else { phi_arc };
// Along-length divisions from the ring (major) radius and the covered arc at // Along-length divisions from the ring (major) radius and the covered arc at
// the LOD's chord tolerance — a short arc samples proportionally less. // the LOD's chord tolerance — a short arc samples proportionally less.
@ -2111,8 +2277,8 @@ pub(crate) fn tess_torus_face(
let phi1 = phi_start + phi_total * ((j + 1) as f64 / nv as f64); let phi1 = phi_start + phi_total * ((j + 1) as f64 / nv as f64);
for i in 0..nu { for i in 0..nu {
let theta0 = TAU * (i as f64 / nu as f64); let theta0 = theta_start + theta_total * (i as f64 / nu as f64);
let theta1 = TAU * ((i + 1) as f64 / nu as f64); let theta1 = theta_start + theta_total * ((i + 1) as f64 / nu as f64);
let p = [ let p = [
torus_pt( torus_pt(
@ -2162,14 +2328,20 @@ fn torus_pt(
theta: f64, // tube angle theta: f64, // tube angle
phi: f64, // revolution angle phi: f64, // revolution angle
) -> [f64; 3] { ) -> [f64; 3] {
// Ring center at angle phi. // Parametric radial direction at angle phi. Keep this independent from the
let ring = [ // signed major radius: a zero-major spindle is still well-defined, while
cx + major_r * (u_dir[0] * phi.cos() + v_dir[0] * phi.sin()), // normalizing `ring - center` would collapse it and would flip the tube
cy + major_r * (u_dir[1] * phi.cos() + v_dir[1] * phi.sin()), // frame whenever the stored major radius is negative.
cz + major_r * (u_dir[2] * phi.cos() + v_dir[2] * phi.sin()), let radial = [
u_dir[0] * phi.cos() + v_dir[0] * phi.sin(),
u_dir[1] * phi.cos() + v_dir[1] * phi.sin(),
u_dir[2] * phi.cos() + v_dir[2] * phi.sin(),
];
let ring = [
cx + major_r * radial[0],
cy + major_r * radial[1],
cz + major_r * radial[2],
]; ];
// Radial direction from torus axis to ring center.
let radial = norm3([ring[0] - cx, ring[1] - cy, ring[2] - cz]);
// Point on tube. // Point on tube.
[ [
ring[0] + minor_r * (radial[0] * theta.cos() + axis[0] * theta.sin()), ring[0] + minor_r * (radial[0] * theta.cos() + axis[0] * theta.sin()),

View file

@ -800,58 +800,53 @@ pub(crate) fn tessellate_entity(
// re-apply the table's scale factor on top of already-baked geometry. // re-apply the table's scale factor on top of already-baked geometry.
// When the block exists we render it directly. Same pattern as // When the block exists we render it directly. Same pattern as
// Dimension's `block_name`. // Dimension's `block_name`.
if let EntityType::Table(tab) = e { if let EntityType::Table(table) = e {
if let Some(br_h) = tab.block_record_handle { if let Some(record) = table.block_record_handle.and_then(|handle| {
if let Some(br) = document.block_records.iter().find(|br| br.handle == br_h) { document
if !br.entity_handles.is_empty() { .block_records
let mut wires: Vec<WireModel> = Vec::with_capacity(br.entity_handles.len()); .iter()
// The Table's own layer style — layer-0 inheritance target .find(|record| record.handle == handle)
// for baked sub-entities on layer "0" (#221). }) {
let tab_l0_color = view::render::adapt_to_bg( if !record.entity_handles.is_empty() {
view::render::layer_render_style(document, &e.common().layer).color, let mut insert =
bg_color, acadrust::entities::Insert::new(
record.name.clone(),
table.insertion_point,
); );
let tab_l0_aci = document insert.rotation = table
.layers .horizontal_direction
.get(&e.common().layer) .y
.map(|l| match &l.color { .atan2(table.horizontal_direction.x);
acadrust::types::Color::Index(i) => *i, insert.common = table.common.clone();
_ => 0,
}) let depths = rustc_hash::FxHashMap::default();
.unwrap_or(0); let graph = crate::scene::render_graph::RenderSceneGraph::new(
// The *T block is laid out in block-local space (origin at document,
// the table's top-left corner); the Table entity carries the None,
// world placement in `insertion_point` + `horizontal_direction` annotation_scale_handle,
// (like an INSERT). Place each baked sub-entity there before true,
// tessellating — without it the whole table renders at the &depths,
// origin. Rotating about the local origin then translating );
// gives world = insertion + R·local; tessellate_entity then let mut wires = Vec::new();
// handles the UTM-scale relative-to-eye split as usual. graph.walk_insert(
let ins = tab.insertion_point; &insert,
let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x); h,
for &eh in &br.entity_handles { |_, _| true,
let Some(sub) = document.get_entity(eh) else { |sub, context| {
continue;
};
let has_book_color = let has_book_color =
view::render::has_resolved_book_color(document, sub); view::render::has_resolved_book_color(document, sub);
let sub_color_is_byblock = !has_book_color let color_byblock = !has_book_color
&& sub.common().color == acadrust::types::Color::ByBlock; && sub.common().color
let sub_is_l0_bylayer = == acadrust::types::Color::ByBlock;
!has_book_color let color_layer0 = !has_book_color
&& view::render::is_effective_layer_zero(&sub.common().layer) && view::render::is_effective_layer_zero(
&& sub.common().color == acadrust::types::Color::ByLayer; &sub.common().layer,
)
&& sub.common().color
== acadrust::types::Color::ByLayer;
let style = context.style_for(document, sub);
let mut placed = sub.clone(); let mut placed = sub.clone();
{ placed.apply_transform(&context.transform);
let ent = placed.as_entity_mut();
if angle.abs() > 1e-9 {
ent.apply_rotation(
acadrust::types::Vector3::new(0.0, 0.0, 1.0),
angle,
);
}
ent.translate(ins);
}
let sub_wires = tessellate_entity( let sub_wires = tessellate_entity(
document, document,
selected, selected,
@ -865,43 +860,39 @@ pub(crate) fn tessellate_entity(
world_per_pixel, world_per_pixel,
paper_space, paper_space,
); );
for mut w in sub_wires { for mut wire in sub_wires {
w.name = h.value().to_string(); wire.name = h.value().to_string();
if sub_color_is_byblock { if color_byblock {
w.color = if sel { wire.color = if sel {
WireModel::SELECTED WireModel::SELECTED
} else { } else {
entity_color view::render::adapt_to_bg(style.0, bg_color)
}; };
w.aci = aci; wire.aci = style.4;
} else if sub_is_l0_bylayer && !sel { } else if color_layer0 && !sel {
w.color = tab_l0_color; wire.color =
w.aci = tab_l0_aci; view::render::adapt_to_bg(style.0, bg_color);
wire.aci = style.4;
} }
wires.push(w); 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 !wires.is_empty() { return wires;
let aabb = entity_aabb(e);
for w in &mut wires {
// Empty SDF-text cells keep their tight glyph-box
// AABB; only stroke/fill wires take the whole-block
// box as a broad-phase pick hint.
if !w.points.is_empty() || !w.fill_tris.is_empty() {
set_wire_aabb(w, aabb);
}
}
return wires;
}
} }
} }
} }
// No baked block (e.g. a table created in-app) — synthesise coloured
// geometry from the rows + TableStyle so fills/colours/borders/margins
// are honoured instead of the monochrome fallback.
let table_anno = 1.0; let table_anno = 1.0;
let mut wires = crate::entities::table::tessellate_table( let mut wires = crate::entities::table::tessellate_table(
tab, table,
document, document,
sel, sel,
entity_color, entity_color,
@ -909,7 +900,7 @@ pub(crate) fn tessellate_entity(
table_anno, table_anno,
); );
for insert in for insert in
crate::entities::table::block_cell_inserts(tab, document, table_anno) crate::entities::table::block_cell_inserts(table, document, table_anno)
{ {
wires.extend(tessellate_entity( wires.extend(tessellate_entity(
document, document,
@ -927,13 +918,10 @@ pub(crate) fn tessellate_entity(
} }
if !wires.is_empty() { if !wires.is_empty() {
let aabb = entity_aabb(e); let aabb = entity_aabb(e);
for w in &mut wires { for wire in &mut wires {
w.aci = aci; wire.aci = aci;
// Empty SDF-text cells keep their tight glyph-box AABB; only if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
// stroke/fill wires take the whole-table box as a broad-phase set_wire_aabb(wire, aabb);
// pick hint (matches the dim / mleader / baked-block paths).
if !w.points.is_empty() || !w.fill_tris.is_empty() {
set_wire_aabb(w, aabb);
} }
} }
return wires; return wires;
@ -986,145 +974,63 @@ pub(crate) fn tessellate_entity(
fill_tris_low: Vec::new(), fill_tris_low: Vec::new(),
}; };
if let Some(cache) = block_cache { let fallback_depths = rustc_hash::FxHashMap::default();
// Xrefs render with the same hue but faded toward `bg_color` so let fallback_cache;
// the user can recognise external-reference geometry at a glance. let cache = match block_cache {
let is_xref = document Some(cache) if cache.defn(&ins.block_name).is_some() => cache,
.block_records _ => {
.get(&ins.block_name) fallback_cache = cache::block_cache::BlockCache::build_for_block(
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.unwrap_or(false);
if let Some(mut wires) = cache::block_cache::expand_insert(
cache,
ins,
h,
ins_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
sel,
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
bg_color,
anno_scale,
) {
// XCLIP: if this INSERT carries an enabled spatial filter,
// clip the expanded block geometry to the boundary polygon so
// only the portion inside the clip is drawn.
if let Some(sf) = pick::xclip::insert_spatial_filter(document, ins) {
let poly = pick::xclip::world_clip_polygon_f64(sf, ins);
pick::xclip::clip_wires(&mut wires, &poly);
// XCLIPFRAME 1/2 shows the boundary itself on screen
// (2 = shown but not plotted).
if document.header.xclip_frame != 0 && poly.len() >= 3 {
wires.push(pick::xclip::frame_wire(
&poly,
format!("{}_xclipframe", h.value()),
ins_color,
sel,
ins_lw_px,
));
}
}
// Per-INSERT attribute values. The block defn carries the
// AttributeDefinitions (templates) which expand_insert skips;
// the AttributeEntity instances live on the Insert itself in
// WCS and need their own tessellation so the user sees the
// values they actually filled in. See #20.
crate::entities::insert::append_insert_attribute_wires(
&mut wires,
document, document,
ins, &ins.block_name,
h,
sel,
ins_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
bg_color,
is_xref,
pslt_factor,
anno_scale, anno_scale,
annotation_scale_handle,
true,
bg_color,
&fallback_depths,
); );
wires.push(marker); &fallback_cache
return wires; }
};
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,
ins,
h,
ins_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
sel,
pslt_factor,
view_aabb,
world_per_pixel,
is_xref,
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);
if document.header.xclip_frame != 0 && polygon.len() >= 3 {
wires.push(pick::xclip::frame_wire(
&polygon,
format!("{}_xclipframe", h.value()),
ins_color,
sel,
ins_lw_px,
));
} }
} }
// Cache miss / unavailable: fall back to the original explode path.
// The block_cache primary path covers all typical Inserts; this
// branch only fires for pathological cache failures.
let br = document.block_records.get(&ins.block_name);
let is_xref = br
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.unwrap_or(false);
let mut wires: Vec<WireModel> = ins
.explode_from_document(document)
.iter()
.cloned()
.map(crate::modules::draw::modify::explode::normalize_insert_entity)
.flat_map(|sub| {
let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px, sub_aci) =
view::render::render_style_for_block_sub(
document,
&sub,
ins_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
);
let sub_color = view::render::adapt_to_bg(sub_color, bg_color);
let sub_color = if is_xref && !sel {
cache::block_cache::fade_toward_bg(sub_color, bg_color)
} else {
sub_color
};
let sub_aabb = entity_aabb(&sub);
let sub_pattern_length = sub_pattern_length * pslt_factor;
let sub_pattern = sub_pattern.map(|v| v * pslt_factor);
let mut wires = convert::tessellate::tessellate(
document,
h,
&sub,
sel,
sub_color,
sub_pattern_length,
sub_pattern,
sub_line_weight_px,
anno_scale,
annotation_scale_handle,
world_per_pixel,
bg_color,
false,
);
for w in &mut wires {
w.name = h.value().to_string();
w.aci = sub_aci;
// Keep the glyph-bounds AABB tessellate set on SDF text wires
// (their geometry is in `text_verts`, not `points`); clobbering
// it here left block text with an UNBOUNDED box. For every
// other wire use the sub-entity box, falling back to the wire's
// own world points when that box is degenerate/unimplemented
// (UNBOUNDED) — otherwise it never culls and stalls snapping on
// block-heavy drawings.
if w.text_verts.is_empty() {
let box_ = if sub_aabb == WireModel::UNBOUNDED_AABB {
wire_points_aabb(w)
} else {
sub_aabb
};
set_wire_aabb(w, box_);
}
}
wires
})
.collect();
crate::entities::insert::append_insert_attribute_wires( crate::entities::insert::append_insert_attribute_wires(
&mut wires, &mut wires,
document, document,

View file

@ -1547,6 +1547,15 @@ pub(crate) fn arrow_from_block(
doc: &CadDocument, doc: &CadDocument,
handle: acadrust::types::Handle, handle: acadrust::types::Handle,
dimasz: f32, dimasz: f32,
) -> ArrowKind {
arrow_from_block_with_deferred_hatch(doc, handle, dimasz, false)
}
pub(crate) fn arrow_from_block_with_deferred_hatch(
doc: &CadDocument,
handle: acadrust::types::Handle,
dimasz: f32,
defer_hatch: bool,
) -> ArrowKind { ) -> ArrowKind {
if handle.is_null() { if handle.is_null() {
return arrow_from_block_name(None, dimasz); return arrow_from_block_name(None, dimasz);
@ -1557,7 +1566,7 @@ pub(crate) fn arrow_from_block(
if let Some(arrow) = builtin_arrow_from_block_name(&record.name, dimasz) { if let Some(arrow) = builtin_arrow_from_block_name(&record.name, dimasz) {
return arrow; return arrow;
} }
custom_arrow_from_block(doc, record, dimasz) custom_arrow_from_block(doc, record, dimasz, defer_hatch)
.unwrap_or_else(|| arrow_from_block_name(None, dimasz)) .unwrap_or_else(|| arrow_from_block_name(None, dimasz))
} }
@ -1653,6 +1662,7 @@ fn custom_arrow_from_block(
doc: &CadDocument, doc: &CadDocument,
record: &acadrust::tables::BlockRecord, record: &acadrust::tables::BlockRecord,
dimasz: f32, dimasz: f32,
defer_hatch: bool,
) -> Option<ArrowKind> { ) -> Option<ArrowKind> {
if record.is_layout() if record.is_layout()
|| record.is_model_space() || record.is_model_space()
@ -1663,17 +1673,42 @@ fn custom_arrow_from_block(
{ {
return None; return None;
} }
let base = block_base_point(doc, record);
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
doc,
None,
None,
true,
&depths,
);
let insert = acadrust::entities::Insert::new(
record.name.clone(),
acadrust::types::Vector3::ZERO,
);
let mut lines = Vec::new(); let mut lines = Vec::new();
let mut fill = Vec::new(); let mut fill = Vec::new();
let mut stack = vec![record.name.clone()]; let mut deferred_hatch = false;
for &entity_handle in &record.entity_handles { graph.walk_insert(
let Some(entity) = doc.get_entity(entity_handle) else { &insert,
continue; record.handle,
}; |_, _| true,
collect_custom_arrow_entity(doc, entity, base, &mut lines, &mut fill, &mut stack, 0); |entity, context| {
} if defer_hatch && matches!(entity, EntityType::Hatch(_)) {
if lines.is_empty() && fill.is_empty() { deferred_hatch = true;
return;
}
let mut placed = entity.clone();
placed.apply_transform(&context.transform);
append_custom_arrow_leaf(
doc,
&placed,
&mut lines,
&mut fill,
);
},
);
if lines.is_empty() && fill.is_empty() && !deferred_hatch {
None None
} else { } else {
Some(ArrowKind::Custom { Some(ArrowKind::Custom {
@ -1684,67 +1719,27 @@ fn custom_arrow_from_block(
} }
} }
fn block_base_point( fn append_custom_arrow_leaf(
doc: &CadDocument,
record: &acadrust::tables::BlockRecord,
) -> acadrust::types::Vector3 {
doc.get_entity(record.block_entity_handle)
.and_then(|entity| match entity {
EntityType::Block(block) => Some(block.base_point),
_ => None,
})
.unwrap_or(acadrust::types::Vector3::ZERO)
}
fn collect_custom_arrow_entity(
doc: &CadDocument, doc: &CadDocument,
entity: &EntityType, entity: &EntityType,
root_base: acadrust::types::Vector3,
lines: &mut Vec<[f32; 3]>, lines: &mut Vec<[f32; 3]>,
fill: &mut Vec<[f32; 3]>, fill: &mut Vec<[f32; 3]>,
stack: &mut Vec<String>,
depth: usize,
) { ) {
if depth >= 32 || entity.common().invisible {
return;
}
match entity { match entity {
EntityType::Block(_) EntityType::Block(_)
| EntityType::BlockEnd(_) | EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_) | EntityType::AttributeDefinition(_)
| EntityType::Dimension(_) | EntityType::Dimension(_)
| EntityType::Leader(_) | EntityType::Leader(_)
| EntityType::MultiLeader(_) => return, | EntityType::MultiLeader(_)
EntityType::Insert(insert) => { | EntityType::Insert(_) => return,
if stack.iter().any(|name| name.eq_ignore_ascii_case(&insert.block_name)) {
return;
}
let Some(record) = doc.block_records.get(&insert.block_name) else {
return;
};
let nested_base = block_base_point(doc, record);
let transform = insert.get_transform();
let transformed_zero = transform.apply(acadrust::types::Vector3::ZERO);
let transformed_base = transform.apply(nested_base);
let correction = transformed_zero - transformed_base;
stack.push(insert.block_name.clone());
for mut child in insert.explode_from_document(doc) {
child.as_entity_mut().translate(correction);
collect_custom_arrow_entity(
doc,
&child,
root_base,
lines,
fill,
stack,
depth + 1,
);
}
stack.pop();
return;
}
EntityType::Hatch(hatch) => { EntityType::Hatch(hatch) => {
append_custom_hatch_fill(hatch, root_base, fill); append_custom_hatch_geometry(
hatch,
acadrust::types::Vector3::ZERO,
lines,
fill,
);
return; return;
} }
_ => {} _ => {}
@ -1766,15 +1761,26 @@ fn collect_custom_arrow_entity(
true, true,
); );
for wire in wires { for wire in wires {
append_custom_wire_points(&wire.points, &wire.points_low, root_base, lines); append_custom_wire_points(
append_custom_fill_points(&wire.fill_tris, &wire.fill_tris_low, root_base, fill); &wire.points,
&wire.points_low,
acadrust::types::Vector3::ZERO,
lines,
);
append_custom_fill_points(
&wire.fill_tris,
&wire.fill_tris_low,
acadrust::types::Vector3::ZERO,
fill,
);
} }
} }
fn append_custom_hatch_fill( fn append_custom_hatch_geometry(
hatch: &acadrust::entities::Hatch, hatch: &acadrust::entities::Hatch,
base: acadrust::types::Vector3, base: acadrust::types::Vector3,
out: &mut Vec<[f32; 3]>, lines: &mut Vec<[f32; 3]>,
fill: &mut Vec<[f32; 3]>,
) { ) {
use lyon_tessellation::math::point; use lyon_tessellation::math::point;
use lyon_tessellation::path::Path; use lyon_tessellation::path::Path;
@ -1786,6 +1792,29 @@ fn append_custom_hatch_fill(
return; return;
}; };
if matches!(
&model.pattern,
crate::scene::model::hatch_model::HatchPattern::Pattern(_)
) {
let z = -base.z as f32;
for [start, end] in model.pattern_segments() {
if !lines.is_empty() && !lines.last().is_some_and(|point| point[0].is_nan()) {
lines.push([f32::NAN; 3]);
}
lines.push([
(start[0] - base.x) as f32,
(start[1] - base.y) as f32,
z,
]);
lines.push([
(end[0] - base.x) as f32,
(end[1] - base.y) as f32,
z,
]);
}
return;
}
let mut builder = Path::builder(); let mut builder = Path::builder();
let mut ring: Vec<[f32; 2]> = Vec::new(); let mut ring: Vec<[f32; 2]> = Vec::new();
let mut finish_ring = |ring: &mut Vec<[f32; 2]>| { let mut finish_ring = |ring: &mut Vec<[f32; 2]>| {
@ -1825,7 +1854,7 @@ fn append_custom_hatch_fill(
return; return;
} }
let z = -base.z as f32; let z = -base.z as f32;
out.extend( fill.extend(
geometry geometry
.indices .indices
.iter() .iter()

View file

@ -853,8 +853,7 @@ impl Scene {
} }
// Reject block-defn-only hatches (entities owned by a // Reject block-defn-only hatches (entities owned by a
// BLOCK record that's neither model nor a paper layout // BLOCK record that's neither model nor a paper layout
// block) — they're tessellated separately via Insert // block) — the scene graph emits only their laid-out copies.
// explosion and only the laid-out copies should appear.
self.belongs_to_visible_block(handle, c.owner_handle, target_block) self.belongs_to_visible_block(handle, c.owner_handle, target_block)
}) })
.flat_map(|(&handle, model)| { .flat_map(|(&handle, model)| {
@ -976,9 +975,8 @@ impl Scene {
} else { } else {
self.bg_color self.bg_color
}; };
// Block-internal hatch fills: descend INSERTs owned by the requested // Instanced/owned hatch leaves are produced by the shared scene graph.
// target only and materialize their fills at world position. models.extend(self.instanced_hatch_models(
models.extend(self.exploded_insert_hatch_models(
target_block, target_block,
hatch_bg, hatch_bg,
true, true,
@ -987,31 +985,23 @@ impl Scene {
all_visible, all_visible,
)); ));
// Wide LwPolyline / Polyline2D bands are no longer hatch fills at // Wide polyline bands remain on the wire path, including inside block
// model level: a flat band is drawn by expanding its centre-line wire // instances; the graph does not reclassify them as hatch fills.
// to `world_width` in the shader (depth-tested, linetype-dashed), and a
// thickened one is extruded into a 3-D tube by the tessellator. Only
// block-internal wide polylines still band via the hatch path (see
// `push_block_wide_fills`), where it scales with the insert transform.
models models
} }
/// Materialize the hatch fills that live *inside* the visible INSERTs of /// Materialize hatch leaves reached through visible scene containers in
/// `layout_block`, baking each INSERT's transform (and nested-INSERT /// `layout_block`, with transforms and inherited styles already resolved.
/// transforms) so the fills land in world position with resolved
/// ByBlock / layer-0 colours.
/// ///
/// Shared by the on-screen hatch set (`synced_hatch_models`) and the /// Shared by the on-screen hatch set (`synced_hatch_models`) and the
/// paper/export hatch set (`paper_canvas_hatches`) so a plot draws /// paper/export hatch set (`paper_canvas_hatches`) so a plot draws
/// block-internal hatches identically to the viewport. Without this the /// block-internal hatches identically to the viewport.
/// export path only saw top-level hatches, so any fill nested in a block
/// INSERT printed as bare monochrome outlines.
/// ///
/// `hatch_bg` adapts pure black/white leaf colours to the target /// `hatch_bg` adapts pure black/white leaf colours to the target
/// background; `tint_selected` re-colours fills of a selected INSERT /// background; `tint_selected` re-colours fills of a selected INSERT
/// (screen highlight) and should be `false` for export. /// (screen highlight) and should be `false` for export.
pub(super) fn exploded_insert_hatch_models( pub(super) fn instanced_hatch_models(
&self, &self,
layout_block: Handle, layout_block: Handle,
hatch_bg: [f32; 4], hatch_bg: [f32; 4],
@ -1020,7 +1010,7 @@ impl Scene {
annotation_scale_handle: Option<Handle>, annotation_scale_handle: Option<Handle>,
all_visible: bool, all_visible: bool,
) -> Vec<HatchModel> { ) -> Vec<HatchModel> {
self.exploded_insert_hatch_models_filtered( self.instanced_hatch_models_filtered(
layout_block, layout_block,
hatch_bg, hatch_bg,
tint_selected, tint_selected,
@ -1058,7 +1048,7 @@ impl Scene {
.flatten() .flatten()
.copied() .copied()
.collect(); .collect();
self.exploded_insert_hatch_models_filtered( self.instanced_hatch_models_filtered(
self.interaction_block_handle(), self.interaction_block_handle(),
hatch_bg, hatch_bg,
true, true,
@ -1070,7 +1060,7 @@ impl Scene {
) )
} }
fn exploded_insert_hatch_models_filtered( fn instanced_hatch_models_filtered(
&self, &self,
layout_block: Handle, layout_block: Handle,
hatch_bg: [f32; 4], hatch_bg: [f32; 4],
@ -1081,400 +1071,105 @@ impl Scene {
targets: Option<&rustc_hash::FxHashSet<Handle>>, targets: Option<&rustc_hash::FxHashSet<Handle>>,
include_preview_hidden: bool, include_preview_hidden: bool,
) -> Vec<HatchModel> { ) -> Vec<HatchModel> {
let layer_hidden = |layer: &str| {
self.document
.layers
.get(layer)
.map(|l| l.flags.off || l.flags.frozen)
.unwrap_or(false)
};
let mut models: Vec<HatchModel> = Vec::new();
// Draw-order composition (#408): every fill inherits its INSERT's
// draw-depth slot, offset by the child's rank INSIDE its block
// (draw_depth_map ranks per owning block), scaled to stay within the
// insert's half-gap in the host ordering. Without this every block /
// xref fill landed at depth 0 and the stack's LIFO pop reversed the
// block's internal stacking.
let depth_map = self.draw_depth_map(); let depth_map = self.draw_depth_map();
let block_count = |name: &str| -> usize { let graph = crate::scene::render_graph::RenderSceneGraph::new(
self.document &self.document,
.block_records frozen,
.get(name) annotation_scale_handle,
.map(|br| br.entity_handles.len()) all_visible,
.unwrap_or(1) depth_map.as_ref(),
.max(1) );
}; let mut hatch_block_memo = std::collections::HashMap::new();
// Exploding an INSERT materializes (clones) every child of its block — let mut models = Vec::new();
// including 3D solids that each carry megabytes of SAB geometry — just graph.walk_root(
// to scan the result for hatch fills. On xref-heavy drawings whose self.render_scene_root(layout_block),
// blocks hold thousands of solids and NO hatches, that cloned ~all block |entity, context| {
// content on every open for nothing (~20 s on a 5-xref BIM aggregate). if context.is_instanced() {
// Skip the explode for blocks with no hatch (transitively) via the same return true;
// memoised `block_has_hatch` scan the pick path uses. Blocks that DO
// contain a hatch still explode exactly as before, so output is unchanged.
let mut hatch_block_memo: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
// Same gate for wide LwPolyline / Polyline2D fills — a block with a wide
// polyline (but no hatch) must still explode so its width band renders
// at world position, like a block-internal hatch. (#222)
let mut wide_block_memo: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
// `explode_from_document` returns a Dimension child verbatim (it carries
// no fill of its own), so a filled arrowhead that lives as a nested
// Insert(hatch) inside the dimension's baked `*D` block never reaches
// this fill explosion. Expand each dimension child's `*D` content into
// the owning block's frame — translate by the dimension insertion_point
// (WCS → block-local) then apply the insert transform (block-local →
// world) — so its arrow inserts / hatches ride the normal walk. Only
// fill-bearing children (Insert / Hatch) are lifted; the dimension's
// lines and text still render through the wire path.
fn explode_including_dims(ins: &DxfInsert, doc: &CadDocument) -> Vec<EntityType> {
let mut out = ins.explode_from_document(doc);
let Some(br) = doc.block_records.get(&ins.block_name) else {
return out;
};
let xform = ins.get_transform();
for &beh in &br.entity_handles {
let Some(EntityType::Dimension(dim)) = doc.get_entity(beh) else {
continue;
};
let bn = dim.base().block_name.clone();
if bn.trim().is_empty() {
continue;
} }
let ins_pt = dim.base().insertion_point; let common = entity.common();
let Some(dblk) = doc if self.object_isolation.hides(common.handle)
.block_records || (!include_preview_hidden
.iter() && self.preview_hidden.contains(&common.handle))
.find(|r| r.name.eq_ignore_ascii_case(&bn)) {
else { return false;
continue;
};
for &deh in &dblk.entity_handles {
let Some(dsub) = doc.get_entity(deh) else {
continue;
};
if !matches!(dsub, EntityType::Insert(_) | EntityType::Hatch(_)) {
continue;
}
let mut placed = dsub.clone();
placed.as_entity_mut().translate(ins_pt);
placed.apply_transform(&xform);
out.push(placed);
} }
} if let Some(targets) = targets {
out return matches!(entity, EntityType::Insert(_))
} && targets.contains(&common.handle);
for entity in self.document.entities() { }
let contextual = crate::scene::annotative::entity_for_annotation_context( match entity {
&self.document, EntityType::Insert(insert) => {
entity, crate::scene::render_graph::block_contains_hatch(
annotation_scale_handle,
);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
if targets.is_some_and(|targets| !targets.contains(&ins.common.handle)) {
continue;
}
let temporarily_hidden = self.object_isolation.hides(ins.common.handle)
|| (!include_preview_hidden && self.preview_hidden.contains(&ins.common.handle));
if ins.common.invisible
|| temporarily_hidden
|| layer_hidden(&ins.common.layer)
{
continue;
}
// Per-viewport freeze: an INSERT on a layer frozen in this content
// viewport hides the whole instance, its internal fills included.
if self.layer_frozen_in(&ins.common.layer, frozen) {
continue;
}
if !self.belongs_to_visible_block(
ins.common.handle,
ins.common.owner_handle,
layout_block,
) {
continue;
}
// Off-scale annotative representation (model space only — paper
// viewports use their per-viewport frozen scale layers). Skips its
// whole fill subtree, matching the wire path.
if crate::scene::annotative::annotative_offscale_for(
&self.document,
&ins.common,
annotation_scale_handle,
all_visible,
) {
continue;
}
if !self.block_has_hatch(&ins.block_name, &mut hatch_block_memo)
&& !self.block_has_wide_poly(&ins.block_name, &mut wide_block_memo)
{
continue;
}
let selected = tint_selected && self.selected.contains(&ins.common.handle);
// XCLIP: clip this insert's exploded hatch fills to the boundary,
// matching how the line geometry is clipped in expand_insert.
let clip_poly = pick::xclip::insert_spatial_filter(&self.document, ins)
.map(|sf| pick::xclip::world_clip_polygon(sf, ins));
// Walk the full block tree: `explode_from_document` only descends
// one level, so nested INSERTs are re-exploded here. Each level
// bakes its transform into the children it returns, so nested
// hatches land in the correct world position. A depth guard keeps
// a malformed cyclic block reference from looping forever.
let normalize = crate::modules::draw::modify::explode::normalize_insert_entity;
// Block-child style inheritance sources (#221), kept raw and
// adapted at the leaf. The INSERT style feeds ByBlock; `l0` (the
// INSERT's layer style) feeds the layer-0 rule. Colour, ACI,
// linetype, and lineweight all chain through nested inserts.
let ins_style =
crate::scene::view::render::render_style_for(&self.document, entity);
let l0 = crate::scene::view::render::layer_render_style(
&self.document,
&ins.common.layer,
);
let layer_aci = |layer: &str| -> u8 {
self.document
.layers
.get(layer)
.and_then(|layer| match &layer.color {
acadrust::types::Color::Index(index) => Some(*index),
_ => None,
})
.unwrap_or(0)
};
let l0_aci = layer_aci(&ins.common.layer);
let [base_depth, half_gap] = depth_map
.get(&ins.common.handle.value())
.copied()
.unwrap_or([0.0, 0.0]);
// Drop off-scale annotative representations (e.g. a 10× copy on
// layer "0 @ 10" when the current scale is 1:1) before `normalize`
// strips their handle — otherwise their whole fill subtree stacks
// under the current-scale copy. Model space only; paper viewports
// use their per-viewport frozen scale layers. `explode` preserves
// the child handle, so the membership lookup still resolves here.
let offscale = |e: &EntityType| -> bool {
matches!(e, EntityType::Insert(ni)
if crate::scene::annotative::annotative_offscale_for(
&self.document,
&ni.common,
annotation_scale_handle,
all_visible,
))
};
type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8);
let mut stack: Vec<(
EntityType,
usize,
ResolvedStyle,
crate::scene::view::render::InheritStyle,
u8,
(f32, f32),
)> = explode_including_dims(ins, &self.document)
.into_iter()
.filter(|e| !offscale(e))
.map(|e| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
&e,
annotation_scale_handle,
)
.into_owned()
})
.map(|e| {
(
normalize(e),
0usize,
ins_style,
l0,
l0_aci,
(base_depth, half_gap),
)
})
.collect();
while let Some((
sub,
depth,
sub_ins_style,
sub_l0,
sub_l0_aci,
(d_base, d_half),
)) = stack.pop()
{
match sub {
EntityType::Insert(nins) => {
if depth >= 32 {
continue;
}
use acadrust::types::Color;
// Chain the inheritance sources into the nested insert,
// mirroring expand_insert's nested resolution: ByBlock →
// parent source; layer-0 + ByLayer → parent layer-0
// target; else the nested insert's own resolved style.
let on_l0 = crate::scene::view::render::is_effective_layer_zero(
&nins.common.layer,
);
let nested_entity = EntityType::Insert(nins.clone());
let has_book_color =
crate::scene::view::render::has_resolved_book_color(
&self.document,
&nested_entity,
);
let mut child_ins_style =
crate::scene::view::render::render_style_for_block_sub(
&self.document,
&nested_entity,
sub_ins_style.0,
sub_ins_style.1,
sub_ins_style.2,
sub_ins_style.3,
sub_l0,
);
child_ins_style.4 = if !has_book_color
&& nins.common.color == Color::ByBlock
{
sub_ins_style.4
} else if !has_book_color
&& on_l0
&& nins.common.color == Color::ByLayer
{
sub_l0_aci
} else {
child_ins_style.4
};
let child_l0 = if on_l0 {
sub_l0
} else {
crate::scene::view::render::layer_render_style(
&self.document,
&nins.common.layer,
)
};
let child_l0_aci = if on_l0 {
sub_l0_aci
} else {
layer_aci(&nins.common.layer)
};
// Nested insert: its slot = parent slot + its rank in
// the parent block, subdivided by its own block size.
let nd_base = d_base
+ depth_map
.get(&nins.common.handle.value())
.map_or(0.0, |d| d[0])
* d_half;
let nd_half = d_half / (block_count(&nins.block_name) as f32 + 1.0);
for e in explode_including_dims(&nins, &self.document) {
if offscale(&e) {
continue;
}
let e =
crate::scene::annotative::entity_for_annotation_context(
&self.document,
&e,
annotation_scale_handle,
)
.into_owned();
stack.push((
normalize(e),
depth + 1,
child_ins_style,
child_l0,
child_l0_aci,
(nd_base, nd_half),
));
}
}
EntityType::Hatch(dxf) => {
if dxf.common.invisible || layer_hidden(&dxf.common.layer) {
continue;
}
// Resolve ByBlock / layer-0 inheritance for this block
// child, including the pattern-stroke lineweight and
// effective ACI used by plot style tables.
let hatch_entity = EntityType::Hatch(dxf.clone());
let mut style = crate::scene::view::render::render_style_for_block_sub(
&self.document, &self.document,
&hatch_entity, &insert.block_name,
sub_ins_style.0, &mut hatch_block_memo,
sub_ins_style.1, )
sub_ins_style.2,
sub_ins_style.3,
sub_l0,
);
let has_book_color =
crate::scene::view::render::has_resolved_book_color(
&self.document,
&hatch_entity,
);
let on_l0 = crate::scene::view::render::is_effective_layer_zero(
&dxf.common.layer,
);
style.4 = if !has_book_color
&& dxf.common.color == acadrust::types::Color::ByBlock
{
sub_ins_style.4
} else if !has_book_color
&& on_l0
&& dxf.common.color == acadrust::types::Color::ByLayer
{
sub_l0_aci
} else {
style.4
};
let color = style.0;
// Explicit ACI 7 solid fills inside blocks are often
// white masks. Keep the explicit white on a light
// layout; adapting it to paper turns the mask into a
// black rectangle unlike the viewport (#618).
let preserve_white_mask = dxf.is_solid
&& matches!(
dxf.common.color,
acadrust::types::Color::Index(7)
);
let color = if preserve_white_mask {
color
} else {
crate::scene::view::render::adapt_to_bg(color, hatch_bg)
};
if let Some(mut model) =
Self::hatch_model_from_dxf(&dxf, color)
{
model.aci = style.4;
model.line_weight_px = style.3;
// In-block rank → within the insert's depth slot.
model.draw_depth = d_base
+ depth_map
.get(&dxf.common.handle.value())
.map_or(0.0, |d| d[0])
* d_half;
if let Some(poly) = &clip_poly {
let clipped = pick::xclip::clip_hatch_boundary(
&model.boundary,
model.world_origin,
poly,
);
if clipped.is_empty() {
continue;
}
model.boundary = std::sync::Arc::new(clipped);
}
if selected {
model.color = [0.15, 0.55, 1.00, model.color[3]];
}
models.push(model);
}
} }
// Wide LwPolyline / Polyline2D bands inside a block are drawn EntityType::Dimension(dimension) => {
// by the shader wire path — block_cache carries each centre- let name = dimension.base().block_name.trim();
// line's `world_width` and scales it by the insert transform !name.is_empty()
// (`emit_wire`) — not as hatch fills here. Same depth-tested, && crate::scene::render_graph::block_contains_hatch(
// linetype-dashed band as at model level. (#222) &self.document,
_ => {} name,
&mut hatch_block_memo,
)
}
_ => true,
} }
} },
} |entity, context| {
if !context.is_instanced() {
return;
}
let EntityType::Hatch(source_hatch) = entity else {
return;
};
let style = context.style_for(&self.document, entity);
let preserve_white_mask = source_hatch.is_solid
&& matches!(
source_hatch.common.color,
acadrust::types::Color::Index(7)
);
let color = if preserve_white_mask {
style.0
} else {
crate::scene::view::render::adapt_to_bg(style.0, hatch_bg)
};
let mut placed = EntityType::Hatch(source_hatch.clone());
placed.apply_transform(&context.transform);
let EntityType::Hatch(hatch) = placed else {
return;
};
let Some(mut model) = Self::hatch_model_from_dxf(&hatch, color) else {
return;
};
model.aci = style.4;
model.line_weight_px = style.3;
model.draw_depth =
context.draw_depth(source_hatch.common.handle, depth_map.as_ref());
for clip in &context.clips {
let clip: Vec<[f32; 2]> = clip
.iter()
.map(|point| [point[0] as f32, point[1] as f32])
.collect();
let clipped = pick::xclip::clip_hatch_boundary(
&model.boundary,
model.world_origin,
&clip,
);
if clipped.is_empty() {
return;
}
model.boundary = std::sync::Arc::new(clipped);
}
if tint_selected && self.selected.contains(&context.root_handle) {
model.color = [0.15, 0.55, 1.00, model.color[3]];
}
models.push(model);
},
);
models models
} }
@ -1487,245 +1182,107 @@ impl Scene {
annotation_scale_handle: Option<Handle>, annotation_scale_handle: Option<Handle>,
all_visible: bool, all_visible: bool,
) -> Vec<HatchModel> { ) -> Vec<HatchModel> {
let is_paper = self.current_layout != "Model"; let bg_color = if self.current_layout != "Model" {
let bg_color: [f32; 4] = if is_paper {
self.paper_bg_color self.paper_bg_color
} else { } else {
self.bg_color self.bg_color
}; };
// No per-frame view-cull here: GPU wipeout buffer upload is self.wipeout_models_for_block_graph(
// gated on geometry_epoch only (see render.rs), so any cull at target_block,
// build time would freeze the visible subset at the geometry frozen,
// epoch boundary and never re-evaluate as the user pans. The annotation_scale_handle,
// pipeline's `wipeout_skip_flags` (compute_wipeout_lod) does all_visible,
// the per-frame skip at draw time instead. bg_color,
false,
)
}
pub(super) fn wipeout_models_for_block_graph(
&self,
target_block: Handle,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
bg_color: [f32; 4],
tint_insert_selection: bool,
) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map(); let depth_map = self.draw_depth_map();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
&self.document,
frozen,
annotation_scale_handle,
all_visible,
depth_map.as_ref(),
);
let mut models = Vec::new(); let mut models = Vec::new();
for entity in self.document.entities() { graph.walk_root(
let EntityType::Wipeout(wo) = entity else { self.render_scene_root(target_block),
continue; |entity, context| {
}; context.is_instanced()
if entity.common().invisible || !self.entity_temporarily_hidden(entity.common().handle)
|| self.entity_temporarily_hidden(wo.common.handle) },
{ |entity, context| {
continue; let EntityType::Wipeout(source) = entity else {
} return;
// Reject block-defn-only wipeouts (owned by a BLOCK record that is };
// neither model nor a paper layout block): those are placed via let mut wipeout = source.clone();
// insert descent below, in world space. Without this they also draw if context.is_instanced() {
// once at their raw block-local coordinates. Mirrors the hatch path. wipeout.insertion_point =
if !self.belongs_to_visible_block( context.transform.apply(source.insertion_point);
wo.common.handle, wipeout.u_vector =
wo.common.owner_handle, context.transform.apply_rotation(source.u_vector);
target_block, wipeout.v_vector =
) { context.transform.apply_rotation(source.v_vector);
continue;
}
if self
.document
.layers
.get(&entity.common().layer)
.map(|l| l.flags.off || l.flags.frozen)
.unwrap_or(false)
{
continue;
}
// Per-viewport freeze: a wipeout on a layer frozen in this content
// viewport is hidden along with the rest of that layer.
if self.layer_frozen_in(&entity.common().layer, frozen) {
continue;
}
// Per-entity world_offset selection so paper-layout content
// viewports still see model-block wipeouts at the right local
// coordinates (same rationale as hatches).
let (fill_origin, boundary) = Self::wipeout_boundary_2d(wo);
if boundary.len() >= 3 {
let mut fill_color = bg_color;
if self.selected.contains(&wo.common.handle) {
fill_color = [0.15, 0.55, 1.00, 0.35];
} }
let (world_origin, mut boundary) =
Self::wipeout_boundary_2d(&wipeout);
for clip in &context.clips {
let clip: Vec<[f32; 2]> = clip
.iter()
.map(|point| [point[0] as f32, point[1] as f32])
.collect();
boundary = pick::xclip::clip_hatch_boundary(
&boundary,
world_origin,
&clip,
);
}
if boundary.len() < 3 {
return;
}
let selection_handle =
if tint_insert_selection && context.is_instanced() {
context.root_handle
} else {
source.common.handle
};
let color = if self.selected.contains(&selection_handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
bg_color
};
models.push(HatchModel { models.push(HatchModel {
boundary: Arc::new(boundary), boundary: Arc::new(boundary),
boundary_wcs: None, boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid, pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(), name: "WIPEOUT_FILL".into(),
color: fill_color, color,
aci: 0, aci: 0,
line_weight_px: 1.0, line_weight_px: 1.0,
angle_offset: 0.0, angle_offset: 0.0,
scale: 1.0, scale: 1.0,
world_origin: fill_origin, world_origin,
// The mask erases what draws below the wipeout and loses draw_depth: context
// to what draws above — including its own frame wire, .draw_depth(source.common.handle, depth_map.as_ref()),
// which rides a +half-rank override on top of this.
draw_depth: depth_map
.get(&wo.common.handle.value())
.map_or(0.0, |d| d[0]),
}); });
} },
} );
// Wipeouts nested inside block inserts: the loop above only sees
// top-level wipeouts, so a wipeout defined inside a block never masked
// once the block was inserted (it did in block-edit, where it is
// top-level). Descend visible inserts and place each block-internal
// wipeout in world space. The insert transform is applied here (apply /
// apply_rotation) rather than through Insert::explode — the latter
// double-scales the u/v basis.
for entity in self.document.entities() {
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
let c = &ins.common;
if c.invisible
|| self.entity_temporarily_hidden(c.handle)
|| self
.document
.layers
.get(&c.layer)
.map(|l| l.flags.off || l.flags.frozen)
.unwrap_or(false)
|| self.layer_frozen_in(&c.layer, frozen)
{
continue;
}
if !self.belongs_to_visible_block(c.handle, c.owner_handle, target_block) {
continue;
}
if crate::scene::annotative::annotative_offscale_for(
&self.document,
c,
annotation_scale_handle,
all_visible,
) {
continue;
}
self.collect_block_wipeouts(
&ins.get_transform(),
&ins.block_name,
0,
frozen,
bg_color,
&depth_map,
&mut models,
annotation_scale_handle,
all_visible,
);
}
models models
} }
/// Recursively collect wipeout masks defined inside a block, transformed to
/// world space by the accumulated insert transform. See `wipeout_models`.
#[allow(clippy::too_many_arguments)]
pub(super) fn collect_block_wipeouts(
&self,
xform: &acadrust::types::Transform,
block_name: &str,
depth: usize,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
models: &mut Vec<HatchModel>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) {
if depth > 32 {
return;
}
let Some(br) = self
.document
.block_records
.iter()
.find(|b| b.name.eq_ignore_ascii_case(block_name))
else {
return;
};
for &eh in &br.entity_handles {
let Some(source) = self.document.get_entity(eh) else {
continue;
};
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let e = contextual.as_ref();
let c = e.common();
if c.invisible
|| self
.document
.layers
.get(&c.layer)
.map(|l| l.flags.off || l.flags.frozen)
.unwrap_or(false)
|| self.layer_frozen_in(&c.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
c,
annotation_scale_handle,
all_visible,
)
{
continue;
}
match e {
EntityType::Wipeout(wo) => {
let mut w = wo.clone();
w.insertion_point = xform.apply(wo.insertion_point);
w.u_vector = xform.apply_rotation(wo.u_vector);
w.v_vector = xform.apply_rotation(wo.v_vector);
let (fill_origin, boundary) = Self::wipeout_boundary_2d(&w);
if boundary.len() >= 3 {
let mut fill_color = bg_color;
if self.selected.contains(&c.handle) {
fill_color = [0.15, 0.55, 1.00, 0.35];
}
models.push(HatchModel {
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: fill_color,
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
draw_depth: depth_map.get(&c.handle.value()).map_or(0.0, |d| d[0]),
});
}
}
EntityType::Insert(ni) => {
// `compose` applies the nested insert's transform first,
// then the accumulated parent transform.
let child = xform.compose(&ni.get_transform());
self.collect_block_wipeouts(
&child,
&ni.block_name,
depth + 1,
frozen,
bg_color,
depth_map,
models,
annotation_scale_handle,
all_visible,
);
}
_ => {}
}
}
}
/// Compute the 2D (XY) boundary polygon for a Wipeout entity. /// Compute the 2D (XY) boundary polygon for a Wipeout entity.
/// Wipeout fill boundary as small f32 offsets from the returned `world_origin` /// Wipeout fill boundary as small f32 offsets from the returned world_origin
/// (the insertion point, kept in f64). Building it in absolute WCS f32 /// (the insertion point, kept in f64).
/// collapsed the boundary into squares at UTM-scale coordinates; the
/// relative-to-eye hatch fill reconstructs `world_origin + offset` precisely.
pub(super) fn wipeout_boundary_2d( pub(super) fn wipeout_boundary_2d(
wo: &acadrust::entities::Wipeout, wo: &acadrust::entities::Wipeout,
) -> ([f64; 2], Vec<[f32; 2]>) { ) -> ([f64; 2], Vec<[f32; 2]>) {

View file

@ -12,6 +12,7 @@ pub mod creation_style;
pub mod model; pub mod model;
pub mod pick; pub mod pick;
pub mod pipeline; pub mod pipeline;
pub(crate) mod render_graph;
pub mod text; pub mod text;
pub mod view; pub mod view;
@ -1699,11 +1700,9 @@ pub struct Scene {
RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<HatchModel>>)>>, RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<HatchModel>>)>>,
frozen_image_cache: RefCell<HashMap<(Handle, u64), (u64, Arc<Vec<ImageModel>>)>>, frozen_image_cache: RefCell<HashMap<(Handle, u64), (u64, Arc<Vec<ImageModel>>)>>,
frozen_mesh_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<MeshLodSet>>)>>, frozen_mesh_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch. /// Cached block-instance hatches for hit-testing, keyed by geometry_epoch.
/// Building this explodes every model-space INSERT, so without the cache a /// The set is geometry-derived, so a camera move or hover never invalidates
/// heavy block-instanced drawing re-explodes thousands of inserts on every /// the shared graph result.
/// hover. The set is geometry-derived, so a camera move / hover never
/// invalidates it.
insert_hatch_cache: RefCell<Option<(u64, u64, Arc<HashMap<Handle, Vec<HatchModel>>>)>>, insert_hatch_cache: RefCell<Option<(u64, u64, Arc<HashMap<Handle, Vec<HatchModel>>>)>>,
/// Sheet "dressing" cache over the unified resident set: the paper sheet /// Sheet "dressing" cache over the unified resident set: the paper sheet
/// drops its own border wire and appends the printable-area guide. /// drops its own border wire and appends the printable-area guide.
@ -5772,6 +5771,7 @@ impl Scene {
None, None,
scale, scale,
self.annotation_all_visible(), self.annotation_all_visible(),
None,
)); ));
self.image_cache self.image_cache
.borrow_mut() .borrow_mut()
@ -5787,164 +5787,82 @@ impl Scene {
frozen: Option<&HashSet<Handle>>, frozen: Option<&HashSet<Handle>>,
annotation_scale_handle: Option<Handle>, annotation_scale_handle: Option<Handle>,
all_visible: bool, all_visible: bool,
viewport: Option<Handle>,
) -> Vec<ImageModel> { ) -> Vec<ImageModel> {
let depth_map = self.draw_depth_map(); let depth_map = self.draw_depth_map();
let mut models: Vec<ImageModel> = self.images let graph = render_graph::RenderSceneGraph::new(
.iter() &self.document,
.filter_map(|(handle, model)| { frozen,
let entity = self.document.get_entity(*handle)?; annotation_scale_handle,
let common = entity.common(); all_visible,
if common.invisible depth_map.as_ref(),
|| self.entity_temporarily_hidden(*handle) );
|| self.layer_hidden(&common.layer) let mut models = Vec::new();
|| self.layer_frozen_in(&common.layer, frozen) let root = if self.block_edit_block.is_none() {
|| !self.belongs_to_visible_block( viewport.map(|viewport| render_graph::SceneRoot::Viewport {
*handle, paper_block: self.current_layout_block_handle(),
common.owner_handle, viewport,
target_block, model_block: target_block,
)
{
return None;
}
let mut m = model.clone();
m.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]);
Some(m)
}) })
.collect(); } else {
for source in self.document.entities() { None
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let EntityType::Insert(insert) = contextual.as_ref() else {
continue;
};
let common = &insert.common;
if common.invisible
|| self.entity_temporarily_hidden(common.handle)
|| self.layer_hidden(&common.layer)
|| self.layer_frozen_in(&common.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
common,
annotation_scale_handle,
all_visible,
)
|| !self.belongs_to_visible_block(
common.handle,
common.owner_handle,
target_block,
)
{
continue;
}
self.collect_block_images(
&insert.block_name,
&insert.get_transform(),
0,
frozen,
annotation_scale_handle,
all_visible,
&depth_map,
&mut models,
);
} }
.unwrap_or_else(|| self.render_scene_root(target_block));
graph.walk_root(
root,
|entity, context| {
context.is_instanced()
|| !self.entity_temporarily_hidden(entity.common().handle)
},
|entity, context| {
let handle = entity.common().handle;
let Some(model) = self.images.get(&handle) else {
return;
};
let mut placed = model.clone();
if context.is_instanced() {
for (corner, low) in
placed.corners.iter_mut().zip(&mut placed.corners_low)
{
let point = acadrust::types::Vector3::new(
corner[0] as f64 + low[0] as f64,
corner[1] as f64 + low[1] as f64,
corner[2] as f64 + low[2] as f64,
);
let point = context.transform.apply(point);
*corner = [point.x as f32, point.y as f32, point.z as f32];
*low = [
(point.x - corner[0] as f64) as f32,
(point.y - corner[1] as f64) as f32,
(point.z - corner[2] as f64) as f32,
];
}
for vertex in &mut placed.verts {
let point = acadrust::types::Vector3::new(
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
);
let point = context.transform.apply(point);
vertex.pos = [
point.x as f32,
point.y as f32,
point.z as f32,
];
vertex.pos_low = [
(point.x - vertex.pos[0] as f64) as f32,
(point.y - vertex.pos[1] as f64) as f32,
(point.z - vertex.pos[2] as f64) as f32,
];
}
}
placed.draw_depth = context.draw_depth(handle, depth_map.as_ref());
models.push(placed);
},
);
models models
} }
#[allow(clippy::too_many_arguments)]
fn collect_block_images(
&self,
block_name: &str,
transform: &acadrust::types::Transform,
depth: usize,
frozen: Option<&HashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
depth_map: &HashMap<u64, [f32; 2]>,
out: &mut Vec<ImageModel>,
) {
if depth > 32 {
return;
}
let Some(record) = self.document.block_records.get(block_name) else {
return;
};
for handle in record.entity_handles.iter().copied() {
let Some(source) = self.document.get_entity(handle) else {
continue;
};
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let entity = contextual.as_ref();
let common = entity.common();
if common.invisible
|| self.layer_hidden(&common.layer)
|| self.layer_frozen_in(&common.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
common,
annotation_scale_handle,
all_visible,
)
{
continue;
}
if let EntityType::Insert(insert) = entity {
let nested = insert.get_transform().then(transform);
self.collect_block_images(
&insert.block_name,
&nested,
depth + 1,
frozen,
annotation_scale_handle,
all_visible,
depth_map,
out,
);
continue;
}
let Some(model) = self.images.get(&handle) else {
continue;
};
let mut placed = model.clone();
for (corner, low) in placed.corners.iter_mut().zip(&mut placed.corners_low) {
let point = acadrust::types::Vector3::new(
corner[0] as f64 + low[0] as f64,
corner[1] as f64 + low[1] as f64,
corner[2] as f64 + low[2] as f64,
);
let point = transform.apply(point);
*corner = [point.x as f32, point.y as f32, point.z as f32];
*low = [
(point.x - corner[0] as f64) as f32,
(point.y - corner[1] as f64) as f32,
(point.z - corner[2] as f64) as f32,
];
}
for vertex in &mut placed.verts {
let point = acadrust::types::Vector3::new(
vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
);
let point = transform.apply(point);
vertex.pos = [point.x as f32, point.y as f32, point.z as f32];
vertex.pos_low = [
(point.x - vertex.pos[0] as f64) as f32,
(point.y - vertex.pos[1] as f64) as f32,
(point.z - vertex.pos[2] as f64) as f32,
];
}
placed.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |value| value[0]);
out.push(placed);
}
}
/// Images owned by the active paper layout block only. The full-canvas /// Images owned by the active paper layout block only. The full-canvas
/// sheet viewport uses this so model-block images don't bleed onto the /// sheet viewport uses this so model-block images don't bleed onto the
/// paper sheet (mirrors `paper_canvas_hatches`). /// paper sheet (mirrors `paper_canvas_hatches`).
@ -5955,6 +5873,7 @@ impl Scene {
None, None,
self.paper_annotation_scale_handle(), self.paper_annotation_scale_handle(),
self.annotation_all_visible(), self.annotation_all_visible(),
None,
)) ))
} }
@ -6020,6 +5939,20 @@ impl Scene {
.unwrap_or_else(|| self.model_space_block_handle()) .unwrap_or_else(|| self.model_space_block_handle())
} }
fn render_scene_root(&self, block: Handle) -> render_graph::SceneRoot {
let role = if self.block_edit_block == Some(block) {
render_graph::BlockRootRole::DefinitionEdit
} else if block == self.model_space_block_handle() {
render_graph::BlockRootRole::ModelSpace
} else {
render_graph::BlockRootRole::PaperSpace
};
render_graph::SceneRoot::Block(render_graph::BlockRoot {
record: block,
role,
})
}
fn interaction_block_handle(&self) -> Handle { fn interaction_block_handle(&self) -> Handle {
if self.block_edit_block.is_none() && self.active_viewport.is_some() { if self.block_edit_block.is_none() && self.active_viewport.is_some() {
self.model_space_block_handle() self.model_space_block_handle()
@ -6318,6 +6251,7 @@ impl Scene {
Some(frozen), Some(frozen),
scale, scale,
all_visible, all_visible,
Some(viewport),
)); ));
self.frozen_image_cache self.frozen_image_cache
.borrow_mut() .borrow_mut()
@ -6434,184 +6368,110 @@ impl Scene {
if self.block_meshes.is_empty() { if self.block_meshes.is_empty() {
return Vec::new(); return Vec::new();
} }
let depth_map = self.draw_depth_map();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
frozen,
annotation_scale_handle,
all_visible,
depth_map.as_ref(),
);
let mut out = Vec::new(); let mut out = Vec::new();
for source in self.document.entities() { graph.walk_root(
let contextual = crate::scene::annotative::entity_for_annotation_context( self.render_scene_root(layout_block),
&self.document, |entity, context| {
source, context.is_instanced()
annotation_scale_handle, || !self.entity_temporarily_hidden(entity.common().handle)
); },
let e = contextual.as_ref(); |entity, context| {
if e.common().owner_handle != layout_block { if !context.is_instanced() {
continue; return;
}
if let EntityType::Insert(ins) = e {
// INSERT on an off/frozen (or invisible) layer hides the whole
// instance, block-internal solids included — including a layer
// frozen only in the requesting content viewport.
if !self.mesh_entity_visible(ins.common.handle)
|| self.layer_frozen_in(&ins.common.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
&ins.common,
annotation_scale_handle,
all_visible,
)
{
continue;
} }
// Colour inheritance sources for block-internal solids (#221). let handle = entity.common().handle;
let bg = self.current_bg(); let Some(set) = self.block_meshes.get(&handle) else {
let ins_color = crate::scene::view::render::adapt_to_bg( return;
crate::scene::view::render::render_style_for(&self.document, e).0,
bg,
);
let l0 = crate::scene::view::render::adapt_to_bg(
crate::scene::view::render::layer_render_style(
&self.document,
&ins.common.layer,
)
.color,
bg,
);
let inherit = BlockMeshInherit {
insert_color: ins_color,
layer0_color: l0,
insert_material:
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
e,
ins_color,
None,
self.material_base_dir.as_deref(),
),
layer0_material:
crate::scene::model::material_model::resolve_layer_material_with_base(
&self.document,
&ins.common.layer,
l0,
self.material_base_dir.as_deref(),
),
}; };
let start = out.len(); let inherit = self.mesh_inherit_for_path(&context.insert_path);
self.expand_block_meshes( let own_alpha =
&ins.block_name, set.lods.first().map(|mesh| mesh.color[3]).unwrap_or(1.0);
&ins.get_transform(), let mut transformed =
0, transform_block_mesh_lod_set(set, &context.transform);
Some(inherit), if let Some(color) = self.block_mesh_override_color(
&mut out, entity,
frozen, handle,
annotation_scale_handle, inherit.as_ref(),
all_visible, own_alpha,
); ) {
// Tag the instanced meshes with the parent INSERT handle so the for mesh in &mut transformed.lods {
// hover / selection highlight (keyed on the mesh name) tints the mesh.color = color;
// block, not the inner solid's own handle which nothing selects.
let name = ins.common.handle.value().to_string();
for set in &mut out[start..] {
for m in &mut set.lods {
m.name = name.clone();
} }
} if let Some(material) = transformed.material.as_mut() {
}
}
out
}
/// Recursively emit transformed instances of a block's solid meshes,
/// composing nested-INSERT transforms. (#123)
fn expand_block_meshes(
&self,
block_name: &str,
accum: &acadrust::types::Transform,
depth: usize,
// Block-child colour inheritance sources, bg-adapted:
// `Some` only on the render path; pick paths pass `None`. Carries both
// colour and AcDbMaterial inheritance for ByBlock/layer-0 children.
inherit: Option<BlockMeshInherit>,
out: &mut Vec<MeshLodSet>,
frozen: Option<&HashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) {
if depth > 16 {
return;
}
let Some(br) = self.document.block_records.get(block_name) else {
return;
};
let handles: Vec<Handle> = br.entity_handles.clone();
for h in handles {
let Some(source) = self.document.get_entity(h) else {
continue;
};
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let e = contextual.as_ref();
// A block-internal solid / nested INSERT on an off/frozen layer
// (or flagged invisible) must not render, same as a top-level one.
if !self.mesh_definition_entity_visible(h)
|| self.layer_frozen_in(&e.common().layer, frozen)
{
continue;
}
if crate::scene::annotative::annotative_offscale_for(
&self.document,
e.common(),
annotation_scale_handle,
all_visible,
) {
continue;
}
if let EntityType::Insert(ins) = e {
let composed = ins.get_transform().then(accum);
let child = inherit
.as_ref()
.map(|parent| self.chain_mesh_inherit(ins, parent));
self.expand_block_meshes(
&ins.block_name,
&composed,
depth + 1,
child,
out,
frozen,
annotation_scale_handle,
all_visible,
);
} else if let Some(set) = self.block_meshes.get(&h) {
// The solid's own transparency (baked into the cached colour).
let own_alpha = set.lods.first().map(|m| m.color[3]).unwrap_or(1.0);
let mut ts = transform_block_mesh_lod_set(set, accum);
// Re-resolve colour against the INSERT context: a block-internal
// solid that is ByBlock or on layer "0" + ByLayer can't be
// coloured at cache-build time (no insert context there). (#221)
if let Some(c) =
self.block_mesh_override_color(e, h, inherit.as_ref(), own_alpha)
{
for lod in &mut ts.lods {
lod.color = c;
}
if let Some(material) = ts.material.as_mut() {
if material.handle.is_none() { if material.handle.is_none() {
material.diffuse = c; material.diffuse = color;
} }
} }
} }
if let Some(material) = if let Some(material) =
self.block_mesh_override_material(e, inherit.as_ref()) self.block_mesh_override_material(entity, inherit.as_ref())
{ {
material.apply_to_with_face_overrides( material.apply_to_with_face_overrides(
&mut ts, &mut transformed,
&self.document, &self.document,
self.material_base_dir.as_deref(), self.material_base_dir.as_deref(),
); );
} }
out.push(ts); let root_name = context.root_handle.value().to_string();
} for mesh in &mut transformed.lods {
mesh.name = root_name.clone();
}
out.push(transformed);
},
);
out
}
fn mesh_inherit_for_path(
&self,
path: &[DxfInsert],
) -> Option<BlockMeshInherit> {
let first = path.first()?;
let entity = EntityType::Insert(first.clone());
let bg = self.current_bg();
let insert_color = crate::scene::view::render::adapt_to_bg(
crate::scene::view::render::render_style_for(&self.document, &entity).0,
bg,
);
let layer0_color = crate::scene::view::render::adapt_to_bg(
crate::scene::view::render::layer_render_style(
&self.document,
&first.common.layer,
)
.color,
bg,
);
let mut inherit = BlockMeshInherit {
insert_color,
layer0_color,
insert_material:
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
&entity,
insert_color,
None,
self.material_base_dir.as_deref(),
),
layer0_material:
crate::scene::model::material_model::resolve_layer_material_with_base(
&self.document,
&first.common.layer,
layer0_color,
self.material_base_dir.as_deref(),
),
};
for insert in &path[1..] {
inherit = self.chain_mesh_inherit(insert, &inherit);
} }
Some(inherit)
} }
/// Background colour for the current layout (model vs paper). /// Background colour for the current layout (model vs paper).
@ -6811,93 +6671,7 @@ impl Scene {
/// Per-Insert hatch models in the current layout, keyed by the Insert /// Per-Insert hatch models in the current layout, keyed by the Insert
/// handle so a click on a block-internal hatch can select the parent /// handle so a click on a block-internal hatch can select the parent
/// Insert (AutoCAD behaviour: sub-entities of a block aren't directly /// Insert; block-owned leaves resolve to their owning instance.
/// selectable; the click resolves to the Insert).
/// Whether a block definition (recursively) contains any Hatch. Memoised in
/// `memo` across calls; lets the pick path skip exploding solid-only blocks.
fn block_has_hatch(
&self,
block_name: &str,
memo: &mut std::collections::HashMap<String, bool>,
) -> bool {
if let Some(&v) = memo.get(block_name) {
return v;
}
// Seed `false` first so a cyclic block reference terminates.
memo.insert(block_name.to_string(), false);
let result = self
.document
.block_records
.get(block_name)
.map(|br| {
br.entity_handles
.iter()
.any(|&h| match self.document.get_entity(h) {
Some(EntityType::Hatch(_)) => true,
Some(EntityType::Insert(ins)) => {
self.block_has_hatch(&ins.block_name, memo)
}
// A dimension bakes its geometry into a per-instance `*D`
// block; a custom filled arrowhead lives there as a nested
// Insert(hatch). Recurse so the fill explosion knows to
// descend the dimension (see `explode_including_dims`).
Some(EntityType::Dimension(dim)) => {
!dim.base().block_name.trim().is_empty()
&& self.block_has_hatch(&dim.base().block_name, memo)
}
_ => false,
})
})
.unwrap_or(false);
memo.insert(block_name.to_string(), result);
result
}
/// Transitively true when a block (or a block it nests) contains a wide
/// LwPolyline / Polyline2D — one carrying a non-zero width. Gate for the
/// block-explode wide-fill pass, mirroring [`Self::block_has_hatch`] so a
/// solid-only block is never exploded just to look for width bands. (#222)
fn block_has_wide_poly(
&self,
block_name: &str,
memo: &mut std::collections::HashMap<String, bool>,
) -> bool {
if let Some(&v) = memo.get(block_name) {
return v;
}
// Seed `false` first so a cyclic block reference terminates.
memo.insert(block_name.to_string(), false);
let result = self
.document
.block_records
.get(block_name)
.map(|br| {
br.entity_handles
.iter()
.any(|&h| match self.document.get_entity(h) {
Some(EntityType::LwPolyline(p)) => {
p.constant_width > 1e-9
|| p.vertices
.iter()
.any(|v| v.start_width > 1e-9 || v.end_width > 1e-9)
}
Some(EntityType::Polyline2D(p)) => {
p.start_width > 1e-9
|| p.end_width > 1e-9
|| p.vertices
.iter()
.any(|v| v.start_width > 1e-9 || v.end_width > 1e-9)
}
Some(EntityType::Insert(ins)) => {
self.block_has_wide_poly(&ins.block_name, memo)
}
_ => false,
})
})
.unwrap_or(false);
memo.insert(block_name.to_string(), result);
result
}
pub fn insert_hatches_for_click(&self) -> Arc<HashMap<Handle, Vec<HatchModel>>> { pub fn insert_hatches_for_click(&self) -> Arc<HashMap<Handle, Vec<HatchModel>>> {
let interaction_block = self.interaction_block_handle(); let interaction_block = self.interaction_block_handle();
@ -6931,13 +6705,26 @@ impl Scene {
let layer_hidden = let layer_hidden =
|layer: &str| self.layer_hidden(layer) || self.interaction_layer_frozen(layer); |layer: &str| self.layer_hidden(layer) || self.interaction_layer_frozen(layer);
let mut out: HashMap<Handle, Vec<HatchModel>> = HashMap::default(); let mut out: HashMap<Handle, Vec<HatchModel>> = HashMap::default();
// Exploding an INSERT to find block-internal hatches is expensive, so // Skip block subtrees that contain no hatch at all. The presence test
// skip blocks that contain no hatch at all (the common case for solid- // is memoised across instances.
// only blocks). The hatch-presence test is memoised across inserts.
let mut hatch_memo: std::collections::HashMap<String, bool> = let mut hatch_memo: std::collections::HashMap<String, bool> =
std::collections::HashMap::new(); std::collections::HashMap::new();
let annotation_scale_handle = self.displayed_annotation_scale_handle(); let annotation_scale_handle = self.displayed_annotation_scale_handle();
let all_visible = self.annotation_all_visible(); let all_visible = self.annotation_all_visible();
let depth_map = self.draw_depth_map();
let frozen: HashSet<Handle> = self
.interaction_viewport_frozen_layers()
.into_iter()
.flatten()
.copied()
.collect();
let graph = render_graph::RenderSceneGraph::new(
&self.document,
(!frozen.is_empty()).then_some(&frozen),
annotation_scale_handle,
all_visible,
depth_map.as_ref(),
);
for entity in self.document.entities() { for entity in self.document.entities() {
let contextual = crate::scene::annotative::entity_for_annotation_context( let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document, &self.document,
@ -6966,33 +6753,52 @@ impl Scene {
) { ) {
continue; continue;
} }
if !self.block_has_hatch(&ins.block_name, &mut hatch_memo) { if !render_graph::block_contains_hatch(
&self.document,
&ins.block_name,
&mut hatch_memo,
) {
continue; continue;
} }
for sub in ins graph.walk_insert(
.explode_from_document(&self.document) ins,
.into_iter() ins.common.handle,
.map(|sub| { |_, _| true,
crate::scene::annotative::entity_for_annotation_context( |entity, context| {
&self.document, let EntityType::Hatch(source) = entity else {
&sub, return;
annotation_scale_handle, };
) let mut placed = EntityType::Hatch(source.clone());
.into_owned() placed.apply_transform(&context.transform);
}) let EntityType::Hatch(hatch) = placed else {
.map(crate::modules::draw::modify::explode::normalize_insert_entity) return;
{ };
let EntityType::Hatch(dxf) = sub else { let color = crate::scene::view::render::adapt_to_bg(
continue; context.style_for(&self.document, entity).0,
}; self.current_bg(),
if dxf.common.invisible || layer_hidden(&dxf.common.layer) { );
continue; let Some(mut model) = Self::hatch_model_from_dxf(&hatch, color)
} else {
let color = self.render_style(&EntityType::Hatch(dxf.clone())).0; return;
if let Some(model) = Self::hatch_model_from_dxf(&dxf, color) { };
for clip in &context.clips {
let clip: Vec<[f32; 2]> = clip
.iter()
.map(|point| [point[0] as f32, point[1] as f32])
.collect();
let clipped = pick::xclip::clip_hatch_boundary(
&model.boundary,
model.world_origin,
&clip,
);
if clipped.is_empty() {
return;
}
model.boundary = Arc::new(clipped);
}
out.entry(ins.common.handle).or_default().push(model); out.entry(ins.common.handle).or_default().push(model);
} },
} );
} }
let arc = Arc::new(out); let arc = Arc::new(out);
*self.insert_hatch_cache.borrow_mut() = *self.insert_hatch_cache.borrow_mut() =

View file

@ -124,7 +124,7 @@ impl MeshMaterial {
description: String::new(), description: String::new(),
diffuse: color, diffuse: color,
ambient: [color[0], color[1], color[2]], ambient: [color[0], color[1], color[2]],
specular: [1.0; 3], specular: [0.08; 3],
gloss: 0.5, gloss: 0.5,
reflectivity: 0.0, reflectivity: 0.0,
self_illumination: 0.0, self_illumination: 0.0,
@ -169,18 +169,11 @@ impl MeshMaterial {
let bump_map = MeshTextureMap::from_dwg(&material.bump_map, base_dir); let bump_map = MeshTextureMap::from_dwg(&material.bump_map, base_dir);
let refraction_map = MeshTextureMap::from_dwg(&material.refraction_map, base_dir); let refraction_map = MeshTextureMap::from_dwg(&material.refraction_map, base_dir);
let normal_map = MeshTextureMap::from_dwg(&material.normal_map, base_dir); let normal_map = MeshTextureMap::from_dwg(&material.normal_map, base_dir);
// A material whose diffuse channel explicitly references an external // The stored diffuse component remains authoritative when an external
// texture is incomplete when that file cannot be resolved. Its stored // image is unavailable. Replacing it with the entity colour would lose
// diffuse RGB is commonly only a red/magenta placeholder (for example // a decoded material value and make the result depend on a guess about
// VILLA_COMPLETE's Plaster material), not the intended appearance. // the missing asset's intended appearance.
// Keep the material's other physical properties, but use the entity's let diffuse = material_color(&material.diffuse_color, entity_color);
// fully resolved Color/ByLayer/ByBlock value for the missing diffuse
// channel.
let diffuse = if diffuse_map.has_content() && diffuse_map.image.is_none() {
entity_color
} else {
material_color(&material.diffuse_color, entity_color)
};
let ambient = material_color(&material.ambient_color, entity_color); let ambient = material_color(&material.ambient_color, entity_color);
let specular = material_color(&material.specular_color, [1.0; 4]); let specular = material_color(&material.specular_color, [1.0; 4]);
let opacity = material.opacity_percent.clamp(0.0, 1.0) as f32; let opacity = material.opacity_percent.clamp(0.0, 1.0) as f32;

View file

@ -84,8 +84,8 @@ pub enum CurvedGen {
phi_min: f32, phi_min: f32,
phi_max: f32, phi_max: f32,
}, },
/// Torus: the two extreme circles of the tube (outer/inner), clipped to the /// Torus: view-dependent tube silhouette, clipped to both parametric
/// revolution window the face covers. /// windows the face covers.
Torus { Torus {
center: [f32; 3], center: [f32; 3],
center_low: [f32; 3], center_low: [f32; 3],
@ -97,6 +97,9 @@ pub enum CurvedGen {
phi_min: f32, phi_min: f32,
phi_span: f32, phi_span: f32,
full: bool, full: bool,
theta_min: f32,
theta_span: f32,
theta_full: bool,
}, },
} }

View file

@ -462,16 +462,15 @@ impl Scene {
} }
// Hatch fills nested inside a block INSERT are owned by the block // Hatch fills nested inside a block INSERT are owned by the block
// record, so the loop above — which only keeps hatches owned by // record, so the loop above — which only keeps hatches owned by
// `layout_block` — never sees them. Explode the layout's visible // `layout_block` — never sees them. Walk the layout's visible block
// INSERTs and materialize their fills at world position, exactly as the // instances and materialize their fills at world position, so export
// viewport does, so the export carries the block's colours instead of // carries the block's colours instead of bare outlines.
// bare outlines. (No selection tint on export.)
let hatch_bg = if self.current_layout != "Model" { let hatch_bg = if self.current_layout != "Model" {
self.paper_bg_color self.paper_bg_color
} else { } else {
self.bg_color self.bg_color
}; };
let exploded = self.exploded_insert_hatch_models( let instanced = self.instanced_hatch_models(
layout_block, layout_block,
hatch_bg, hatch_bg,
false, false,
@ -479,7 +478,7 @@ impl Scene {
annotation_scale_handle, annotation_scale_handle,
all_visible, all_visible,
); );
models.extend(exploded); models.extend(instanced);
Arc::new(models) Arc::new(models)
} }
@ -554,7 +553,7 @@ impl Scene {
} }
models.push(hatch); models.push(hatch);
} }
models.extend(self.exploded_insert_hatch_models( models.extend(self.instanced_hatch_models(
block, block,
self.paper_bg_color, self.paper_bg_color,
false, false,
@ -565,8 +564,8 @@ impl Scene {
models models
} }
/// Plot-only wipeout masks for a specific block. Nested inserts follow the /// Plot-only wipeout masks for a specific block. Nested instances follow
/// same recursive collector as the on-screen model path. /// the same scene graph as the on-screen model path.
pub(super) fn plot_wipeouts_for_block( pub(super) fn plot_wipeouts_for_block(
&self, &self,
block: Handle, block: Handle,
@ -575,96 +574,14 @@ impl Scene {
all_visible: bool, all_visible: bool,
highlight_selection: bool, highlight_selection: bool,
) -> Vec<HatchModel> { ) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map(); self.wipeout_models_for_block_graph(
let mut models = Vec::new(); block,
for entity in self.document.entities() { frozen,
let EntityType::Wipeout(wipeout) = entity else { annotation_scale_handle,
continue; all_visible,
}; self.paper_bg_color,
let common = &wipeout.common; highlight_selection,
if common.invisible )
|| self.entity_temporarily_hidden(common.handle)
|| self
.document
.layers
.get(&common.layer)
.map(|layer| layer.flags.off || layer.flags.frozen)
.unwrap_or(false)
|| self.layer_frozen_in(&common.layer, frozen)
|| !self.belongs_to_visible_block(common.handle, common.owner_handle, block)
{
continue;
}
let (world_origin, boundary) = Self::wipeout_boundary_2d(wipeout);
if boundary.len() < 3 {
continue;
}
models.push(HatchModel {
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: if highlight_selection && self.selected.contains(&common.handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
self.paper_bg_color
},
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
scale: 1.0,
world_origin,
draw_depth: depth_map
.get(&common.handle.value())
.map_or(0.0, |depth| depth[0]),
});
}
for entity in self.document.entities() {
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
);
let EntityType::Insert(insert) = contextual.as_ref() else {
continue;
};
let common = &insert.common;
if common.invisible
|| self.entity_temporarily_hidden(common.handle)
|| self
.document
.layers
.get(&common.layer)
.map(|layer| layer.flags.off || layer.flags.frozen)
.unwrap_or(false)
|| self.layer_frozen_in(&common.layer, frozen)
|| crate::scene::annotative::annotative_offscale_for(
&self.document,
common,
annotation_scale_handle,
all_visible,
)
|| !self.belongs_to_visible_block(common.handle, common.owner_handle, block)
{
continue;
}
self.collect_block_wipeouts(
&insert.get_transform(),
&insert.block_name,
0,
frozen,
if highlight_selection && self.selected.contains(&common.handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
self.paper_bg_color
},
&depth_map,
&mut models,
annotation_scale_handle,
all_visible,
);
}
models
} }
/// Paper-layout wipeout fills (paper hit-testing / export). Same rationale as /// Paper-layout wipeout fills (paper hit-testing / export). Same rationale as

View file

@ -12,7 +12,7 @@
use acadrust::entities::Insert; use acadrust::entities::Insert;
use acadrust::objects::{ObjectType, SpatialFilter}; use acadrust::objects::{ObjectType, SpatialFilter};
use acadrust::types::{Handle, Vector3}; use acadrust::types::{Handle, Transform, Vector3};
use acadrust::CadDocument; use acadrust::CadDocument;
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -62,25 +62,23 @@ fn dict_entry(doc: &CadDocument, dict: Handle, key: &str) -> Option<Handle> {
/// insert was later rescaled the stored `M⁻¹` still places the clip correctly.) /// insert was later rescaled the stored `M⁻¹` still places the clip correctly.)
/// Two points describe a rectangle (opposite corners); three or more an /// Two points describe a rectangle (opposite corners); three or more an
/// explicit polygon. /// explicit polygon.
pub fn world_clip_polygon( /// Build the clip boundary in absolute f64 world coordinates so clipping at
sf: &SpatialFilter, /// large coordinate values remains precise.
ins: &Insert, #[cfg(test)]
) -> Vec<[f32; 2]> { fn world_clip_polygon_f64(
world_clip_polygon_f64(sf, ins)
.into_iter()
.map(|[x, y]| [x as f32, y as f32])
.collect()
}
/// f64 variant of [`world_clip_polygon`]. The boundary stays in absolute world
/// coordinates so clipping at UTM scale (~5.7e6) is precise — the f32 version
/// quantizes each vertex by ~0.5 m, which warps the clip region and breaks both
/// the clipped render and ZOOM Extents.
pub fn world_clip_polygon_f64(
sf: &SpatialFilter, sf: &SpatialFilter,
ins: &Insert, ins: &Insert,
) -> Vec<[f64; 2]> { ) -> Vec<[f64; 2]> {
let xform = ins.get_transform(); world_clip_polygon_for_transform(sf, &ins.get_transform())
}
/// Build the clip boundary with the exact transform used by the corresponding
/// block instance. Callers that resolve block base points or parent instances
/// pass their composed transform here so geometry and clipping share one space.
pub fn world_clip_polygon_for_transform(
sf: &SpatialFilter,
xform: &Transform,
) -> Vec<[f64; 2]> {
let inv_block = &sf.inverse_block_transform; let inv_block = &sf.inverse_block_transform;
let local: Vec<[f64; 2]> = if sf.boundary_points.len() == 2 { let local: Vec<[f64; 2]> = if sf.boundary_points.len() == 2 {
let a = sf.boundary_points[0]; let a = sf.boundary_points[0];
@ -764,13 +762,13 @@ mod tests {
ins.set_x_scale(0.1); ins.set_x_scale(0.1);
ins.set_y_scale(0.1); ins.set_y_scale(0.1);
let poly = world_clip_polygon(&sf, &ins); let poly = world_clip_polygon_f64(&sf, &ins);
// vert (580,4528) → ×1000 → (580000,4528000) → ×0.1 + insert → // vert (580,4528) → ×1000 → (580000,4528000) → ×0.1 + insert →
// (639668, 4516955). // (639668, 4516955).
let xs: Vec<f32> = poly.iter().map(|p| p[0]).collect(); let xs: Vec<f64> = poly.iter().map(|p| p[0]).collect();
let ys: Vec<f32> = poly.iter().map(|p| p[1]).collect(); let ys: Vec<f64> = poly.iter().map(|p| p[1]).collect();
let minx = xs.iter().cloned().fold(f32::MAX, f32::min); let minx = xs.iter().cloned().fold(f64::MAX, f64::min);
let miny = ys.iter().cloned().fold(f32::MAX, f32::min); let miny = ys.iter().cloned().fold(f64::MAX, f64::min);
assert!((minx - 639668.0).abs() < 1.0, "minx={minx}"); assert!((minx - 639668.0).abs() < 1.0, "minx={minx}");
assert!((miny - 4516955.0).abs() < 1.0, "miny={miny}"); assert!((miny - 4516955.0).abs() < 1.0, "miny={miny}");
} }

View file

@ -406,6 +406,16 @@ struct MaterialMapParams {
blends1: [f32; 4], blends1: [f32; 4],
/// Presence bits for the same four maps. /// Presence bits for the same four maps.
present1: [u32; 4], present1: [u32; 4],
/// Tiling modes for diffuse, specular, reflection and opacity.
tiling0: [u32; 4],
/// Tiling modes for bump, refraction, normal and reserved.
tiling1: [u32; 4],
/// Two-sided, normal-map method, global-illumination and final-gather modes.
render_modes: [u32; 4],
/// Advanced-data-present, anonymous and two reserved source-state bits.
source_state: [u32; 4],
/// Color-bleed scale and reserved render values.
indirect: [f32; 4],
} }
fn upload_rgba_texture( fn upload_rgba_texture(
@ -490,7 +500,7 @@ pub fn create_material_bind_group(
); );
let sampler = |label: &'static str, tiling: u8| { let sampler = |label: &'static str, tiling: u8| {
let address = match tiling { let address = match tiling {
1 => wgpu::AddressMode::Repeat, 0 | 1 => wgpu::AddressMode::Repeat,
4 => wgpu::AddressMode::MirrorRepeat, 4 => wgpu::AddressMode::MirrorRepeat,
_ => wgpu::AddressMode::ClampToEdge, _ => wgpu::AddressMode::ClampToEdge,
}; };
@ -525,6 +535,21 @@ pub fn create_material_bind_group(
let bump_sampler = sampler("mesh.material.bump_sampler", tiling(4)); let bump_sampler = sampler("mesh.material.bump_sampler", tiling(4));
let refraction_sampler = sampler("mesh.material.refraction_sampler", tiling(5)); let refraction_sampler = sampler("mesh.material.refraction_sampler", tiling(5));
let normal_sampler = sampler("mesh.material.normal_sampler", tiling(6)); let normal_sampler = sampler("mesh.material.normal_sampler", tiling(6));
let channel_flags = material.map_or(0x7f, |material| material.channel_flags as u32);
let channel_present = |channel: usize, bit: u32| {
material.is_some_and(|material| {
let maps = [
&material.diffuse_map,
&material.specular_map,
&material.reflection_map,
&material.opacity_map,
&material.bump_map,
&material.refraction_map,
&material.normal_map,
];
channel_flags & bit != 0 && maps[channel].image.is_some()
}) as u32
};
let params = MaterialMapParams { let params = MaterialMapParams {
blends0: material.map_or([0.0; 4], |material| { blends0: material.map_or([0.0; 4], |material| {
[ [
@ -534,12 +559,12 @@ pub fn create_material_bind_group(
material.opacity_map.blend_factor, material.opacity_map.blend_factor,
] ]
}), }),
present0: material.map_or([0; 4], |material| { present0: material.map_or([0; 4], |_material| {
[ [
material.diffuse_map.image.is_some() as u32, channel_present(0, 0x01),
material.specular_map.image.is_some() as u32, channel_present(1, 0x02),
material.reflection_map.image.is_some() as u32, channel_present(2, 0x04),
material.opacity_map.image.is_some() as u32, channel_present(3, 0x08),
] ]
}), }),
blends1: material.map_or([0.0; 4], |material| { blends1: material.map_or([0.0; 4], |material| {
@ -552,12 +577,48 @@ pub fn create_material_bind_group(
}), }),
present1: material.map_or([0; 4], |material| { present1: material.map_or([0; 4], |material| {
[ [
material.bump_map.image.is_some() as u32, channel_present(4, 0x10),
material.refraction_map.image.is_some() as u32, channel_present(5, 0x20),
material.normal_map.image.is_some() as u32, material.normal_map.image.is_some() as u32
* (material.normal_map_method == 0) as u32,
0, 0,
] ]
}), }),
tiling0: material.map_or([1; 4], |material| {
[
material.diffuse_map.tiling as u32,
material.specular_map.tiling as u32,
material.reflection_map.tiling as u32,
material.opacity_map.tiling as u32,
]
}),
tiling1: material.map_or([1; 4], |material| {
[
material.bump_map.tiling as u32,
material.refraction_map.tiling as u32,
material.normal_map.tiling as u32,
1,
]
}),
render_modes: material.map_or([1, 0, 0, 0], |material| {
[
material.two_sided as u32,
material.normal_map_method as u32,
material.global_illumination as u32,
material.final_gather as u32,
]
}),
source_state: material.map_or([0; 4], |material| {
[
material.advanced_data_present as u32,
material.is_anonymous as u32,
0,
0,
]
}),
indirect: material.map_or([1.0, 0.0, 0.0, 0.0], |material| {
[material.color_bleed_scale, 0.0, 0.0, 0.0]
}),
}; };
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.material.params"), label: Some("mesh.material.params"),
@ -656,51 +717,45 @@ pub fn create_material_bind_group(
} }
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct MaterialBatchKey([u32; 16]); struct MaterialBatchKey([u32; 40]);
fn material_key( fn material_key(
material: Option<&crate::scene::model::material_model::MeshMaterial>, material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4], color: [f32; 4],
) -> MaterialBatchKey { ) -> MaterialBatchKey {
let mut key = [0u32; 40];
key[2..6].copy_from_slice(&color.map(f32::to_bits));
let Some(material) = material else { let Some(material) = material else {
return MaterialBatchKey([ return MaterialBatchKey(key);
0,
0,
color[0].to_bits(),
color[1].to_bits(),
color[2].to_bits(),
color[3].to_bits(),
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]);
}; };
let handle = material.handle.map_or(0, |handle| handle.value()); let handle = material.handle.map_or(0, |handle| handle.value());
MaterialBatchKey([ key[0] = handle as u32;
handle as u32, key[1] = (handle >> 32) as u32;
(handle >> 32) as u32, key[6..10].copy_from_slice(&material.diffuse.map(f32::to_bits));
material.diffuse[0].to_bits(), key[10..13].copy_from_slice(&material.ambient.map(f32::to_bits));
material.diffuse[1].to_bits(), key[13..16].copy_from_slice(&material.specular.map(f32::to_bits));
material.diffuse[2].to_bits(), key[16] = material.gloss.to_bits();
material.diffuse[3].to_bits(), key[17] = material.reflectivity.to_bits();
material.specular[0].to_bits(), key[18] = material.self_illumination.to_bits();
material.specular[1].to_bits(), key[19] = material.translucence.to_bits();
material.specular[2].to_bits(), key[20] = material.refraction_index.to_bits();
material.gloss.to_bits(), key[21] = material.luminance.to_bits();
material.reflectivity.to_bits(), key[22] = material.two_sided as u32;
material.self_illumination.to_bits(), key[23] = material.illumination_model as u32;
material.luminance.to_bits(), key[24] = material.channel_flags as u32;
material.refraction_index.to_bits(), key[25] = material.mode as u32;
material.diffuse_map.projection as u32, key[26] = material.indirect_bump_scale.to_bits();
material.diffuse_map.tiling as u32, key[27] = material.reflectance_scale.to_bits();
]) key[28] = material.transmittance_scale.to_bits();
key[29] = material.luminance_mode as u32;
key[30] = material.normal_map_method as u32;
key[31] = material.normal_map_strength.to_bits();
key[32] = material.is_anonymous as u32;
key[33] = material.global_illumination as u32;
key[34] = material.final_gather as u32;
key[35] = material.color_bleed_scale.to_bits();
key[36] = material.advanced_data_present as u32;
MaterialBatchKey(key)
} }
fn morton_axis(mut value: u32) -> u64 { fn morton_axis(mut value: u32) -> u64 {
@ -736,6 +791,7 @@ struct MeshBatchPart<'a> {
set: &'a MeshLodSet, set: &'a MeshLodSet,
mesh: &'a MeshModel, mesh: &'a MeshModel,
display_mesh: &'a MeshModel, display_mesh: &'a MeshModel,
uv_mesh: &'a MeshModel,
material: Option<&'a crate::scene::model::material_model::MeshMaterial>, material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4], color: [f32; 4],
indices: Vec<u32>, indices: Vec<u32>,
@ -772,6 +828,7 @@ fn build_instanced_chunk(
let material = first.material; let material = first.material;
let color = first.color; let color = first.color;
let has_normals = mesh.normals.len() == mesh.verts.len(); let has_normals = mesh.normals.len() == mesh.verts.len();
let bounds = mesh_bounds(mesh);
let (material_params, specular, ambient, advanced, flags) = let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material); material_vertex_params(material);
let vertex = |index: usize, edge: bool| { let vertex = |index: usize, edge: bool| {
@ -799,7 +856,15 @@ fn build_instanced_chunk(
let uvs = if edge { let uvs = if edge {
[[0.0; 2]; 7] [[0.0; 2]; 7]
} else { } else {
material_uvs(material, position, normal) material_uvs(
material,
position,
normal,
bounds,
position,
normal,
bounds,
)
}; };
MeshVertex { MeshVertex {
position, position,
@ -856,7 +921,7 @@ fn build_instanced_chunk(
]); ]);
} }
} }
let transparent = color[3] < 0.999; let transparent = material_is_transparent(material, color);
let face_indices = first.include_faces.then_some(first.indices.as_slice()); let face_indices = first.include_faces.then_some(first.indices.as_slice());
let opaque_indices = if transparent { let opaque_indices = if transparent {
&[][..] &[][..]
@ -932,11 +997,80 @@ fn build_instanced_chunk(
)) ))
} }
fn mesh_bounds(mesh: &MeshModel) -> [f32; 6] {
let mut bounds = [
f32::INFINITY,
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
];
for (index, high) in mesh.verts.iter().copied().enumerate() {
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
for axis in 0..3 {
let value = high[axis] + low[axis];
bounds[axis] = bounds[axis].min(value);
bounds[axis + 3] = bounds[axis + 3].max(value);
}
}
bounds
}
fn material_uses_model_mapper(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> bool {
material.is_some_and(|material| {
[
&material.diffuse_map,
&material.specular_map,
&material.reflection_map,
&material.opacity_map,
&material.bump_map,
&material.refraction_map,
&material.normal_map,
]
.into_iter()
.any(|map| map.image.is_some() && map.auto_transform & 4 != 0)
})
}
fn material_is_transparent(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
) -> bool {
color[3] < 0.999
|| material.is_some_and(|material| {
material.translucence > 0.0
|| (material.channel_flags as u32 & 0x08 != 0
&& material.opacity_map.image.is_some())
})
}
fn material_map_uv( fn material_map_uv(
map: &crate::scene::model::material_model::MeshTextureMap, map: &crate::scene::model::material_model::MeshTextureMap,
position: [f32; 3], local_position: [f32; 3],
normal: [f32; 3], local_normal: [f32; 3],
local_bounds: [f32; 6],
model_position: [f32; 3],
model_normal: [f32; 3],
model_bounds: [f32; 6],
) -> [f32; 2] { ) -> [f32; 2] {
let (mut position, normal, bounds) = if map.auto_transform & 4 != 0 {
(model_position, model_normal, model_bounds)
} else {
(local_position, local_normal, local_bounds)
};
if map.auto_transform & 2 != 0 {
for axis in 0..3 {
let extent = bounds[axis + 3] - bounds[axis];
position[axis] = if extent.is_finite() && extent.abs() > f32::EPSILON {
(position[axis] - bounds[axis]) / extent
} else {
0.0
};
}
}
let m = &map.transform; let m = &map.transform;
let p = [ let p = [
position[0] * m[0] + position[1] * m[1] + position[2] * m[2] + m[3], position[0] * m[0] + position[1] * m[1] + position[2] * m[2] + m[3],
@ -975,20 +1109,24 @@ fn material_map_uv(
fn material_uvs( fn material_uvs(
material: Option<&crate::scene::model::material_model::MeshMaterial>, material: Option<&crate::scene::model::material_model::MeshMaterial>,
position: [f32; 3], local_position: [f32; 3],
normal: [f32; 3], local_normal: [f32; 3],
local_bounds: [f32; 6],
model_position: [f32; 3],
model_normal: [f32; 3],
model_bounds: [f32; 6],
) -> [[f32; 2]; 7] { ) -> [[f32; 2]; 7] {
let Some(material) = material else { let Some(material) = material else {
return [[0.0; 2]; 7]; return [[0.0; 2]; 7];
}; };
[ [
material_map_uv(&material.diffuse_map, position, normal), material_map_uv(&material.diffuse_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.specular_map, position, normal), material_map_uv(&material.specular_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.reflection_map, position, normal), material_map_uv(&material.reflection_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.opacity_map, position, normal), material_map_uv(&material.opacity_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.bump_map, position, normal), material_map_uv(&material.bump_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.refraction_map, position, normal), material_map_uv(&material.refraction_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.normal_map, position, normal), material_map_uv(&material.normal_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
] ]
} }
@ -998,7 +1136,7 @@ fn material_vertex_params(
let Some(material) = material else { let Some(material) = material else {
return ( return (
[0.5, 0.0, 0.0, 0.0], [0.5, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0], [0.08, 0.08, 0.08, 1.0],
[0.3, 0.3, 0.3, 0.0], [0.3, 0.3, 0.3, 0.0],
[1.0; 4], [1.0; 4],
[0, 127, 0, 0], [0, 127, 0, 0],
@ -1089,11 +1227,11 @@ pub fn build_mesh_batch_filtered(
}) { }) {
continue; continue;
} }
let mesh = set let source_mesh = set
.instance_source .instance_source
.as_ref() .as_ref()
.and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty())) .and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty()));
.unwrap_or(display_mesh); let mesh = source_mesh.unwrap_or(display_mesh);
let triangle_count = display_mesh.indices.len() / 3; let triangle_count = display_mesh.indices.len() / 3;
let has_face_materials = let has_face_materials =
display_mesh.triangle_material_handles.len() == triangle_count display_mesh.triangle_material_handles.len() == triangle_count
@ -1119,6 +1257,7 @@ pub fn build_mesh_batch_filtered(
set, set,
mesh, mesh,
display_mesh, display_mesh,
uv_mesh: mesh,
material: set.material.as_ref(), material: set.material.as_ref(),
color, color,
indices: display_mesh.indices.clone(), indices: display_mesh.indices.clone(),
@ -1166,6 +1305,7 @@ pub fn build_mesh_batch_filtered(
set, set,
mesh, mesh,
display_mesh, display_mesh,
uv_mesh: mesh,
material, material,
color, color,
indices, indices,
@ -1211,6 +1351,7 @@ pub fn build_mesh_batch_filtered(
let eligible = storage_instancing let eligible = storage_instancing
&& part.set.instance_transform.is_some() && part.set.instance_transform.is_some()
&& part.set.instance_source.is_some() && part.set.instance_source.is_some()
&& !material_uses_model_mapper(part.material)
&& part.mesh.verts.len() <= max_verts && part.mesh.verts.len() <= max_verts
&& part.indices.len() / 3 <= max_tris && part.indices.len() / 3 <= max_tris
&& part && part
@ -1336,6 +1477,9 @@ pub fn build_mesh_batch_filtered(
let has_normals = mesh.normals.len() == mesh.verts.len(); let has_normals = mesh.normals.len() == mesh.verts.len();
let (material_params, specular, ambient, advanced, flags) = let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material); material_vertex_params(material);
let uv_mesh = part.uv_mesh;
let local_bounds = mesh_bounds(uv_mesh);
let model_bounds = mesh_bounds(mesh);
let edge_color = set let edge_color = set
.visual_style .visual_style
.as_ref() .as_ref()
@ -1346,7 +1490,25 @@ pub fn build_mesh_batch_filtered(
} else { } else {
[0.0, 1.0, 0.0] [0.0, 1.0, 0.0]
}; };
let uv = material_uvs(material, mesh.verts[vi], normal); let local_position = uv_mesh
.verts
.get(vi)
.copied()
.unwrap_or(mesh.verts[vi]);
let local_normal = uv_mesh
.normals
.get(vi)
.copied()
.unwrap_or(normal);
let uv = material_uvs(
material,
local_position,
local_normal,
local_bounds,
mesh.verts[vi],
normal,
model_bounds,
);
MeshVertex { MeshVertex {
position: mesh.verts[vi], position: mesh.verts[vi],
normal, normal,
@ -1368,7 +1530,7 @@ pub fn build_mesh_batch_filtered(
}; };
// A solid whose baked colour is not fully opaque routes into the // A solid whose baked colour is not fully opaque routes into the
// transparent index stream so it is drawn last, without depth writes. // transparent index stream so it is drawn last, without depth writes.
let is_transp = part_color[3] < 0.999; let is_transp = material_is_transparent(material, part_color);
let mesh_tris = part.indices.len() / 3; let mesh_tris = part.indices.len() / 3;
if part.include_faces { if part.include_faces {
total_tris += mesh_tris as u64; total_tris += mesh_tris as u64;

View file

@ -2109,7 +2109,7 @@ impl Pipeline {
} }
CurvedGen::Torus { CurvedGen::Torus {
center, center_low, axis, u_dir, v_dir, major, minor, center, center_low, axis, u_dir, v_dir, major, minor,
phi_min, phi_span, full, phi_min, phi_span, full, theta_min, theta_span, theta_full,
} => { } => {
let ctr = lo(*center, *center_low); let ctr = lo(*center, *center_low);
let (axis, u, v) = (d3(*axis), d3(*u_dir), d3(*v_dir)); let (axis, u, v) = (d3(*axis), d3(*u_dir), d3(*v_dir));
@ -2136,12 +2136,17 @@ impl Pipeline {
let cur = [th, th + std::f64::consts::PI]; let cur = [th, th + std::f64::consts::PI];
for k in 0..2 { for k in 0..2 {
let t = cur[k]; let t = cur[k];
let p = ring + (radial * t.cos() + axis * t.sin()) * minor; let theta_offset =
if let Some(pp) = prev[k] { (t - *theta_min as f64).rem_euclid(std::f64::consts::TAU);
let on_face = *theta_full || theta_offset <= *theta_span as f64;
let p = on_face.then(|| {
ring + (radial * t.cos() + axis * t.sin()) * minor
});
if let (Some(pp), Some(p)) = (prev[k], p) {
verts.push(mk(pp)); verts.push(mk(pp));
verts.push(mk(p)); verts.push(mk(p));
} }
prev[k] = Some(p); prev[k] = p;
} }
} }
} }
@ -2280,7 +2285,7 @@ impl Pipeline {
WireGpu::from_batch(device, face3d_wires, depth_map, self.wire_const_bgl.as_ref()); WireGpu::from_batch(device, face3d_wires, depth_map, self.wire_const_bgl.as_ref());
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face // Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
// tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`); // tris go to `chunks_3d` (gated by `keep_3d_mesh_fills`);
// 2D fills (text-LOD greek, MultiLeader background) go to // 2D fills (text-LOD greek, MultiLeader background, dimension arrows) go to
// `chunks_2d`. The 3-D wireframe additionally removes only legacy // `chunks_2d`. The 3-D wireframe additionally removes only legacy
// planar SOLID interiors; HATCH is handled by a separate pass. // planar SOLID interiors; HATCH is handled by a separate pass.
let keep_3d_mesh_fills = !wireframe_only; let keep_3d_mesh_fills = !wireframe_only;
@ -2289,11 +2294,11 @@ impl Pipeline {
}; };
let has_any_2d_fill = all_wires let has_any_2d_fill = all_wires
.iter() .iter()
.any(|w| !w.fill_tris.is_empty() && w.points.is_empty() && !solid_fill_hidden(w)); .any(|w| !w.fill_tris.is_empty() && !w.fill_is_3d && !solid_fill_hidden(w));
let has_any_3d_fill = !face3d_wires.is_empty() let has_any_3d_fill = !face3d_wires.is_empty()
|| all_wires || all_wires
.iter() .iter()
.any(|w| !w.fill_tris.is_empty() && !w.points.is_empty()); .any(|w| !w.fill_tris.is_empty() && w.fill_is_3d);
let has_fills = has_any_2d_fill || (keep_3d_mesh_fills && has_any_3d_fill); let has_fills = has_any_2d_fill || (keep_3d_mesh_fills && has_any_3d_fill);
if !has_fills { if !has_fills {
self.gpu_face3d_fill = None; self.gpu_face3d_fill = None;

709
src/scene/render_graph.rs Normal file
View file

@ -0,0 +1,709 @@
//! 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,
render_style_for, render_style_for_block_sub, 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) -> 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),
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) -> Self {
Self {
insert: render_style_for(document, entity),
layer0: layer_render_style(document, &entity.common().layer),
layer0_aci: layer_aci(document, &entity.common().layer),
}
}
pub fn for_nested(
document: &CadDocument,
insert: &Insert,
parent: Self,
) -> Self {
InsertStyleSpec::new(document, insert).resolve(parent)
}
pub fn for_owned(
document: &CadDocument,
entity: &EntityType,
parent: Option<Self>,
) -> Self {
let on_layer0 = is_effective_layer_zero(&entity.common().layer);
let insert = parent
.map(|style| style.resolve(document, entity))
.unwrap_or_else(|| render_style_for(document, entity));
Self {
insert,
layer0: if on_layer0 {
parent
.map(|style| style.layer0)
.unwrap_or_else(|| layer_render_style(document, &entity.common().layer))
} else {
layer_render_style(document, &entity.common().layer)
},
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, entity: &EntityType) -> ResolvedStyle {
let mut resolved = render_style_for_block_sub(
document,
entity,
self.insert.0,
self.insert.1,
self.insert.2,
self.insert.3,
self.layer0,
);
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,
}
impl RenderContext {
fn direct(depth_base: f32) -> 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,
}
}
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))
.unwrap_or_else(|| render_style_for(document, entity))
}
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]>,
}
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,
}
}
/// 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 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);
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);
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))
.unwrap_or_else(|| BlockStyle::for_entity(self.document, &insert_entity));
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),
{
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,
));
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);
}
_ => {}
}
}
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 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
}

View file

@ -37,6 +37,11 @@ struct MaterialMaps {
present0: vec4<u32>, present0: vec4<u32>,
blends1: vec4<f32>, blends1: vec4<f32>,
present1: vec4<u32>, present1: vec4<u32>,
tiling0: vec4<u32>,
tiling1: vec4<u32>,
render_modes: vec4<u32>,
source_state: vec4<u32>,
indirect: vec4<f32>,
}; };
@group(1) @binding(0) var diffuse_map: texture_2d<f32>; @group(1) @binding(0) var diffuse_map: texture_2d<f32>;
@ -186,8 +191,20 @@ fn vs_edge(
return out; return out;
} }
fn map_weight(uv: vec2<f32>, tiling: u32) -> f32 {
if (tiling == 2u) {
return select(
0.0,
1.0,
all(uv >= vec2<f32>(0.0)) && all(uv <= vec2<f32>(1.0)),
);
}
return 1.0;
}
@fragment @fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> { fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let two_sided = maps.render_modes.x != 0u;
var n: vec3<f32>; var n: vec3<f32>;
if (u.flat_shade > 0.5) { if (u.flat_shade > 0.5) {
// Per-triangle face normal: derivatives of the interpolated // Per-triangle face normal: derivatives of the interpolated
@ -197,7 +214,6 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
} else { } else {
n = normalize(in.normal); n = normalize(in.normal);
} }
// Build a derivative tangent frame from the generated material UVs. This // Build a derivative tangent frame from the generated material UVs. This
// works for planar/box/cylindrical/spherical AcDbMaterial projections and // works for planar/box/cylindrical/spherical AcDbMaterial projections and
// does not require ACIS to carry explicit texture-coordinate vertices. // does not require ACIS to carry explicit texture-coordinate vertices.
@ -218,7 +234,9 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let mapped = normalize( let mapped = normalize(
tangent * sampled.x + bitangent * sampled.y + n * sampled.z tangent * sampled.x + bitangent * sampled.y + n * sampled.z
); );
let strength = maps.blends1.z * max(in.advanced.x, 0.0); let strength = maps.blends1.z
* map_weight(in.uv_normal, maps.tiling1.z)
* max(in.advanced.x, 0.0);
n = normalize(mix(n, mapped, clamp(strength, 0.0, 1.0))); n = normalize(mix(n, mapped, clamp(strength, 0.0, 1.0)));
} else if (maps.present1.x != 0u) { } else if (maps.present1.x != 0u) {
let height = dot( let height = dot(
@ -226,7 +244,12 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
vec3<f32>(0.2126, 0.7152, 0.0722), vec3<f32>(0.2126, 0.7152, 0.0722),
); );
let slope = tangent * dpdx(height) + bitangent * dpdy(height); let slope = tangent * dpdx(height) + bitangent * dpdy(height);
n = normalize(n - slope * maps.blends1.x * max(in.advanced.y, 0.0) * 4.0); n = normalize(
n - slope
* maps.blends1.x
* map_weight(in.uv_bump, maps.tiling1.x)
* 4.0
);
} }
// Native AcDbLight / Sun lighting. When a drawing has no active lights, // Native AcDbLight / Sun lighting. When a drawing has no active lights,
@ -236,9 +259,9 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let l1 = normalize(vec3<f32>(-0.7, 0.3, 0.4)); // fill (left) let l1 = normalize(vec3<f32>(-0.7, 0.3, 0.4)); // fill (left)
let l2 = normalize(vec3<f32>( 0.2, -0.6, -0.8)); // back/under let l2 = normalize(vec3<f32>( 0.2, -0.6, -0.8)); // back/under
var direct_light = vec3<f32>( var direct_light = vec3<f32>(
0.45 * abs(dot(n, l0)) 0.45 * select(max(dot(n, l0), 0.0), abs(dot(n, l0)), two_sided)
+ 0.30 * abs(dot(n, l1)) + 0.30 * select(max(dot(n, l1), 0.0), abs(dot(n, l1)), two_sided)
+ 0.25 * abs(dot(n, l2)) + 0.25 * select(max(dot(n, l2), 0.0), abs(dot(n, l2)), two_sided)
); );
var key_light = l0; var key_light = l0;
if (u.lighting.x > 0.5) { if (u.lighting.x > 0.5) {
@ -281,9 +304,12 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
} }
} }
let strength = max(direction_intensity.w, 0.0) * attenuation; let strength = max(direction_intensity.w, 0.0) * attenuation;
direct_light += color_hotspot.rgb let response = select(
* max(dot(n, light_vector), 0.0) max(dot(n, light_vector), 0.0),
* strength; abs(dot(n, light_vector)),
two_sided,
);
direct_light += color_hotspot.rgb * response * strength;
if (index == 0u) { if (index == 0u) {
key_light = light_vector; key_light = light_vector;
} }
@ -298,26 +324,36 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
); );
let fresnel = fresnel0 let fresnel = fresnel0
+ (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0); + (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0);
let specular_strength = clamp(0.08 + in.material.y + fresnel, 0.0, 1.5); let specular_strength = clamp(1.0 + in.material.y + fresnel, 0.0, 1.5);
var specular_color = in.specular.rgb; var albedo = in.color.rgb;
if (maps.present0.x != 0u) {
let texel = textureSample(diffuse_map, diffuse_sampler, in.uv_diffuse).rgb;
let blend = clamp(
maps.blends0.x * map_weight(in.uv_diffuse, maps.tiling0.x),
0.0,
1.0,
);
albedo = mix(albedo, texel, blend);
}
var specular_color = select(in.specular.rgb, albedo, in.flags.x == 1u);
if (maps.present0.y != 0u) { if (maps.present0.y != 0u) {
let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb; let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb;
specular_color = mix(specular_color, texel, clamp(maps.blends0.y, 0.0, 1.0)); let blend = clamp(
maps.blends0.y * map_weight(in.uv_specular, maps.tiling0.y),
0.0,
1.0,
);
specular_color = mix(specular_color, texel, blend);
} }
let half_response = select( let half_response = select(
abs(dot(n, half_vec)),
max(dot(n, half_vec), 0.0), max(dot(n, half_vec), 0.0),
u.lighting.x > 0.5, abs(dot(n, half_vec)),
two_sided,
); );
let specular = specular_color let specular = specular_color
* pow(half_response, gloss_exp) * pow(half_response, gloss_exp)
* specular_strength * specular_strength
* max(in.advanced.z, 0.0); * max(in.advanced.z, 0.0);
var albedo = in.color.rgb;
if (maps.present0.x != 0u) {
let texel = textureSample(diffuse_map, diffuse_sampler, in.uv_diffuse).rgb;
albedo = mix(albedo, texel, clamp(maps.blends0.x, 0.0, 1.0));
}
let ambient_light = albedo * clamp(in.ambient.rgb, vec3<f32>(0.0), vec3<f32>(1.0)); let ambient_light = albedo * clamp(in.ambient.rgb, vec3<f32>(0.0), vec3<f32>(1.0));
var lit = ambient_light + albedo * clamp(direct_light, vec3<f32>(0.0), vec3<f32>(2.0)) + specular; var lit = ambient_light + albedo * clamp(direct_light, vec3<f32>(0.0), vec3<f32>(2.0)) + specular;
if (maps.present0.z != 0u) { if (maps.present0.z != 0u) {
@ -329,7 +365,14 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
lit = mix( lit = mix(
lit, lit,
reflected, reflected,
clamp(maps.blends0.z * in.material.y * max(in.advanced.z, 0.0), 0.0, 1.0), clamp(
maps.blends0.z
* map_weight(in.uv_reflection, maps.tiling0.z)
* in.material.y
* max(in.advanced.z, 0.0),
0.0,
1.0,
),
); );
} }
if (maps.present1.y != 0u) { if (maps.present1.y != 0u) {
@ -341,15 +384,29 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
lit = mix( lit = mix(
lit, lit,
transmitted, transmitted,
clamp(maps.blends1.y * in.ambient.a * max(in.advanced.w, 0.0), 0.0, 1.0), clamp(
maps.blends1.y
* map_weight(in.uv_refraction, maps.tiling1.y)
* in.ambient.a
* max(in.advanced.w, 0.0),
0.0,
1.0,
),
); );
} }
let emission = clamp(in.material.z + in.material.w, 0.0, 1.0); var rgb = mix(lit, albedo, clamp(in.material.z, 0.0, 1.0));
let rgb = mix(lit, albedo, emission); if (in.flags.w == 1u) {
rgb = lit + albedo * max(in.material.w, 0.0);
}
var material_alpha = in.color.a; var material_alpha = in.color.a;
if (maps.present0.w != 0u) { if (maps.present0.w != 0u) {
let opacity = textureSample(opacity_map, opacity_sampler, in.uv_opacity).r; let opacity = textureSample(opacity_map, opacity_sampler, in.uv_opacity).r;
material_alpha *= mix(1.0, opacity, clamp(maps.blends0.w, 0.0, 1.0)); let blend = clamp(
maps.blends0.w * map_weight(in.uv_opacity, maps.tiling0.w),
0.0,
1.0,
);
material_alpha *= mix(1.0, opacity, blend);
} }
material_alpha *= 1.0 - clamp(in.ambient.a * max(in.advanced.w, 0.0), 0.0, 0.95); material_alpha *= 1.0 - clamp(in.ambient.a * max(in.advanced.w, 0.0), 0.0, 0.95);
let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5); let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5);