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 committed 2026-08-05 21:44:59 +03:00
1 parent afc94dce13
commit b667eeeb05
17 files changed
+2228 -1881

No files matched your search

+32 -7
View File
@@ -29,14 +29,19 @@ fn material_map_text(map: &acadrust::objects::MaterialMap) -> String {
}
}
2 => format!(
"Procedural {}",
map.texture.as_ref().map_or(0, |texture| texture.mode)
"Procedural {:#?}",
map.texture
),
_ => "None".to_string(),
0 => "Scene".to_string(),
value => format!("Source {value}"),
};
format!(
"{source}; blend {:.3}; projection {}; tiling {}; auto {}",
map.blend_factor, map.projection, map.tiling, map.auto_transform
"{source}; blend {:.3}; projection {}; tiling {}; auto {}; transform {:?}",
map.blend_factor,
map.projection,
map.tiling,
map.auto_transform,
map.transform
)
}
@@ -568,11 +573,31 @@ impl OpenCADStudio {
t!("Advanced").as_ref(),
"mat_advanced",
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.indirect_bump_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
),
),
],
+30 -4
View File
@@ -1309,7 +1309,8 @@ use acadrust::{CadDocument, EntityType, Handle};
use crate::scene::convert::tess_util::aci_to_rgba;
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};
@@ -1660,6 +1661,16 @@ fn tessellate_dimension_inner(
// 3. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end).
// Unknown / NULL block handles fall back to ClosedFilled.
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 t = ArrowKind::Tick {
size: (dimtsz_raw as f32).max(0.001),
@@ -1668,11 +1679,26 @@ fn tessellate_dimension_inner(
} else if let Some(s) = style {
if dimsah {
(
arrow_from_block(document, s.dimblk1, dimasz),
arrow_from_block(document, s.dimblk2, dimasz),
arrow_from_block_with_deferred_hatch(
document,
s.dimblk1,
dimasz,
defer_arrow_hatches,
),
arrow_from_block_with_deferred_hatch(
document,
s.dimblk2,
dimasz,
defer_arrow_hatches,
),
)
} 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)
}
} else {
+39 -26
View File
@@ -1538,9 +1538,9 @@ impl MultiLeaderTess for MultiLeader {
// ── Block content ───────────────────────────────────────────────────────
// When content_type == Block, the MultiLeader displays a block reference
// at block_content_location with the recorded rotation/scale. Synthesize
// an Insert and explode it through the standard tessellator. The block
// resolves via block_content_handle (handle → block_record name).
// at block_content_location with the recorded rotation/scale. A
// synthetic Insert supplies that context to the shared scene graph;
// every child then uses its normal tessellator.
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
let block_name = match ml.block_content_handle {
Some(h) if !h.is_null() => document
@@ -1567,29 +1567,42 @@ impl MultiLeaderTess for MultiLeader {
// anchor when *creating* the multileader; at render time the
// file's stored leader endpoints already encode that choice.
let _ = ml.block_connection_type;
for sub in synth_ins.explode_from_document(document) {
let normalized =
crate::modules::draw::modify::explode::normalize_insert_entity(sub);
let mut sub_wires = tessellate(
document,
handle,
&normalized,
selected,
block_color,
leader_pat_len,
leader_pat,
leader_lw_px,
1.0,
None,
None,
bg_color,
false,
);
for w in &mut sub_wires {
w.name = name.clone();
}
wires.extend(sub_wires);
}
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
document,
None,
None,
true,
&depths,
);
graph.walk_insert(
&synth_ins,
handle,
|_, _| true,
|entity, context| {
let mut placed = entity.clone();
placed.apply_transform(&context.transform);
let mut sub_wires = tessellate(
document,
handle,
&placed,
selected,
block_color,
leader_pat_len,
leader_pat,
leader_lw_px,
1.0,
None,
None,
bg_color,
false,
);
for w in &mut sub_wires {
w.name = name.clone();
}
wires.extend(sub_wires);
},
);
// Block attributes attached to the multileader — render each as
// its own attribute entity at WCS location like INSERT does.
for ba in &ml.block_attributes {
+147 -178
View File
@@ -100,27 +100,7 @@ pub struct LocalWire {
pub struct NestedRef {
pub block_name: String,
pub xform: Transform,
/// Nested INSERT's own resolved style (used when child wires need
/// 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 style: crate::scene::render_graph::InsertStyleSpec,
pub instance_offsets: Vec<[f64; 3]>,
/// XCLIP boundary for this nested insert, in the parent defn's local frame
/// (`None` = unclipped). Baked at build time because the clip's spatial
@@ -142,6 +122,7 @@ pub enum LocalSub {
#[derive(Clone, Debug, Default)]
pub struct BlockDefn {
pub subs: Vec<LocalSub>,
pub base_point: Vector3,
/// Union of every sub's local AABB (including nested-INSERT contributions
/// resolved at expand time via their own defn's `aabb_local`). XY only —
/// the wire renderer is 2D-dominant. Expressed in this defn's *offset*
@@ -149,8 +130,8 @@ pub struct BlockDefn {
pub aabb_local: [f32; 4],
/// Raw entity count of the source block record (`entity_handles.len()`).
/// Divisor for nested depth composition: a nested insert's children get a
/// sub-range of `parent_scale / (child_count + 1)` — the same formula the
/// exploded-fill path uses, so bands and fills stay on one depth scale.
/// sub-range of `parent_scale / (child_count + 1)`, shared with the scene
/// graph so bands and fills stay on one depth scale.
pub child_count: usize,
}
@@ -203,8 +184,7 @@ impl BlockCache {
all_visible: bool,
bg_color: [f32; 4],
// Scene draw-depth map ([depth, half] per handle) — source of each
// block child's in-block rank, so band depth composition agrees with
// the SortEntitiesTable-aware ranking the fill explosion uses.
// block child's in-block rank, shared by cached wires and graph leaves.
depth_map: &HashMap<u64, [f32; 2]>,
) -> Self {
use crate::par::prelude::*;
@@ -249,6 +229,41 @@ impl BlockCache {
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
/// contribute their own aabb_local; nested INSERT references look up 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()
}
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.
fn layer_hidden(doc: &CadDocument, layer: &str) -> bool {
doc.layers
@@ -441,79 +476,67 @@ fn build_defn(
subs.push(LocalSub::Wire(wire));
}
}
// A table nested in a block bakes its geometry (gridlines, cell
// text, fills) into a `*T` block, laid out in block-LOCAL space with
// the table carrying the world placement in insertion_point +
// horizontal_direction (like an INSERT). The top-level path expands
// and places it in `tessellate_entity`; the block-expand path skips
// that, so a nested table drew nothing. Place each `*T` sub by the
// table's rotation+insertion here (defn-local), then tessellate — the
// parent insert transform composes on top at expand time.
EntityType::Table(tab) => {
let tblk = tab
.block_record_handle
.and_then(|h| doc.block_records.iter().find(|br| br.handle == h));
let mut used_baked = false;
if let Some(tblk) = tblk {
used_baked = !tblk.entity_handles.is_empty();
let ins = tab.insertion_point;
let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x);
for &teh in &tblk.entity_handles {
let Some(tsub) = doc.get_entity(teh) else {
continue;
};
if tsub.common().invisible || layer_hidden(doc, &tsub.common().layer) {
continue;
}
let mut placed = tsub.clone();
{
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,
);
// A nested table's stored graphics use the same scene-graph
// traversal as top-level table content. The resulting leaf wires
// remain local to this cached definition; the parent instance
// transform is composed later by the cache expansion.
EntityType::Table(table) => {
let baked = table.block_record_handle.and_then(|handle| {
doc.block_records
.iter()
.find(|record| record.handle == handle)
});
if let Some(record) = baked.filter(|record| {
!record.entity_handles.is_empty()
}) {
let mut insert = acadrust::entities::Insert::new(
record.name.clone(),
table.insertion_point,
);
insert.rotation = table
.horizontal_direction
.y
.atan2(table.horizontal_direction.x);
insert.common = table.common.clone();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
doc,
None,
annotation_scale_handle,
all_visible,
depth_map,
);
graph.walk_insert(
&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);
// route those through the nested-ref path like the
// dimension arrows above.
if let EntityType::Insert(cell_ins) = &placed {
subs.push(LocalSub::Nested(build_nested_ref(
cell_ins, doc, bg_color, depth_map,
)));
} else {
for lw in
tessellate_sub_local(
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));
},
);
} else {
for wire in tessellate_sub_local(
doc,
entity,
anno_scale,
annotation_scale_handle,
bg_color,
depth_map,
) {
subs.push(LocalSub::Wire(wire));
}
for insert in crate::entities::table::block_cell_inserts(
tab,
table,
doc,
anno_scale,
) {
@@ -546,6 +569,7 @@ fn build_defn(
}
BlockDefn {
subs,
base_point: crate::scene::render_graph::block_base_point(doc, block_name),
aabb_local: [0.0; 4],
child_count: br.entity_handles.len(),
}
@@ -557,49 +581,22 @@ fn build_nested_ref(
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
) -> 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 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
// its clip when the parent block is expanded — the spatial filter object
// 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)
.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 {
block_name: nested_ins.block_name.clone(),
xform: nested_ins.get_transform(),
ins_color,
ins_pat_len,
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),
xform,
style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins),
instance_offsets: crate::scene::render_graph::array_offsets(nested_ins),
clip_poly,
local_rank: depth_map
.get(&nested_ins.common.handle.value())
@@ -877,7 +874,9 @@ pub fn expand_insert(
anno_scale: f32,
) -> Option<Vec<WireModel>> {
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
// marked with the AcAnnotativeData XDATA) scale as ONE uniform unit about
// 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] {
xform.clone()
} else {
@@ -1475,38 +1474,25 @@ fn expand_defn(
// itself on layer "0" with ByLayer props inherits the outer
// layer-0 target (so its ByBlock leaves resolve to that layer,
// not layer 0). Mirrors the leaf resolution in emit_wire.
let nested_color = if nref.color_is_byblock {
ctx.ins_color
} else if nref.layer_is_zero && nref.color_is_bylayer {
ctx.l0.color
} else {
nref.ins_color
let parent_style = crate::scene::render_graph::BlockStyle {
insert: (
ctx.ins_color,
ctx.ins_pat_len,
ctx.ins_pat,
ctx.ins_lw_px,
0,
),
layer0: ctx.l0,
layer0_aci: 0,
};
let (nested_pat_len, nested_pat) = if nref.lt_is_byblock {
(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 nested_style = nref.style.resolve(parent_style);
let inner_ctx = ExpandCtx {
cache: ctx.cache,
ins_color: nested_color,
ins_pat_len: nested_pat_len,
ins_pat: nested_pat,
ins_lw_px: nested_lw_px,
l0: nested_l0,
ins_color: nested_style.insert.0,
ins_pat_len: nested_style.insert.1,
ins_pat: nested_style.insert.2,
ins_lw_px: nested_style.insert.3,
l0: nested_style.layer0,
selected: ctx.selected,
pslt_factor: ctx.pslt_factor,
view_aabb: ctx.view_aabb,
@@ -1516,7 +1502,7 @@ fn expand_defn(
};
visited.push(nref.block_name.clone());
// 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 = (
d_range.0 + nref.local_rank * d_range.1,
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,
}
}
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
}
+267 -95
View File
@@ -4,8 +4,8 @@
// • plane-surface faces → collect coedge-loop polygon, fan-triangulate.
// • cone-surface faces → sample a parametric grid (handles both cylinders
// and true cones).
// • sphere-surface faces → sample a full UV grid.
// • torus-surface faces → sample a full UV grid.
// • sphere-surface faces → sample a boundary-clipped UV grid.
// • torus-surface faces → sample a boundary-clipped UV grid.
//
// Coverage is tracked explicitly. Unsupported or malformed faces retain
// 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 major = torus.major_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 (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.
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 t in 0..m {
let t0 = TAU * (t as f64 / m as f64);
let t1 = TAU * ((t + 1) as f64 / m as f64);
let t0 = theta_min + theta_total * (t 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, 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.
let ring_segs = edge_arc_segs(major, phi_total).max(2);
let n_ring = count.max(2);
for k in 0..n_ring {
let theta = TAU * (k as f64 / n_ring as f64);
for theta in iso_params(theta_min, theta_span, theta_full, n_ring) {
for s in 0..ring_segs {
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);
@@ -622,54 +626,24 @@ fn sphere_param_range(
/// 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;
/// the arc between them is the opening. But the tube body can also carry
/// interior hole loops where another solid punches through it, so the widest
/// empty gap is *not* reliably the opening — a hole splits the body into wide
/// hole-free stretches that masquerade as it. Only a gap flanked by two end
/// caps is genuinely surface-free, so that is the opening; the body is the rest
/// of the turn. Returns `(body_start, body_span, full)`.
/// A partial tube ends in two minor-circle caps sitting in constant-φ planes.
/// Their adjacent planar faces carry outward normals that identify which cap is
/// the start and which is the end, so both short elbows and long open tubes use
/// the correct side of the analytic surface. If that topology is absent or
/// ambiguous, the face remains untrimmed instead of guessing between the two
/// possible arcs. Returns `(body_start, body_span, full)`.
pub(crate) fn torus_phi_range(
sat: &SatDocument,
face: &SatFace,
center: [f64; 3],
u: [f64; 3],
v: [f64; 3],
major: f64,
) -> (f64, f64, bool) {
// Below this revolution-angle spread a loop is a minor circle sitting in a
// constant-φ plane — a tube end cap. Interference loops (where another solid
// punches through the tube) wander several degrees in φ, well above it.
const CAP_SPREAD: f64 = 0.15; // rad (~8.6°)
// 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();
const GEOM_EPS: f64 = 1e-6;
let axis = norm3(cross3(u, v));
let mut starts = Vec::new();
let mut ends = Vec::new();
let mut lp = face.first_loop();
let mut seen: HashSet<i32> = HashSet::default();
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 {
break;
};
let poly = collect_loop_polygon(sat, &sl, BOUNDARY_CHORD_FRAC);
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;
}
let phis: Vec<f64> = poly.iter().map(|&p| phi_of(p)).collect();
let is_cap = spread(&phis) < CAP_SPREAD;
for phi in phis {
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]
let physical_phi = radial_v.atan2(radial_u);
let phi = if major < 0.0 {
(physical_phi - std::f64::consts::PI).rem_euclid(TAU)
} else {
(tagged[0].0 + TAU, tagged[0].1)
physical_phi.rem_euclid(TAU)
};
let gap = phi_j - phi_i;
if cap_i && cap_j && gap > MIN_OPENING && gap < opening_gap {
opening_gap = gap;
body_start = phi_j.rem_euclid(TAU); // body resumes past the far cap
let major_sign = if major < 0.0 { -1.0 } else { 1.0 };
let tangent = norm3([
major_sign * (-u[0] * phi.sin() + v[0] * phi.cos()),
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);
}
(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
@@ -868,7 +1011,22 @@ fn collect_curved_gens(
torus.u_direction().2,
]);
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]);
out.push(CurvedGen::Torus {
center,
@@ -881,6 +1039,9 @@ fn collect_curved_gens(
phi_min: pmin as f32,
phi_span: pspan as f32,
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 major_r = torus.major_radius();
let minor_r = torus.minor_radius();
// Tube cross-section is a closed minor circle → full-circle segment count at
// the tube (minor) radius.
let nu = lod.circle_segs(minor_r).max(3); // around the tube
let (theta_start, theta_arc, theta_full) =
torus_theta_range(sat, face, [cx, cy, cz], axis, major_r, minor_r);
let theta_total = if theta_full { TAU } else { theta_arc };
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")
// 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 };
// Along-length divisions from the ring (major) radius and the covered arc at
// 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);
for i in 0..nu {
let theta0 = TAU * (i as f64 / nu as f64);
let theta1 = TAU * ((i + 1) as f64 / nu as f64);
let theta0 = theta_start + theta_total * (i as f64 / nu as f64);
let theta1 = theta_start + theta_total * ((i + 1) as f64 / nu as f64);
let p = [
torus_pt(
@@ -2162,14 +2328,20 @@ fn torus_pt(
theta: f64, // tube angle
phi: f64, // revolution angle
) -> [f64; 3] {
// Ring center at angle phi.
// Parametric radial direction at angle phi. Keep this independent from the
// signed major radius: a zero-major spindle is still well-defined, while
// normalizing `ring - center` would collapse it and would flip the tube
// frame whenever the stored major radius is negative.
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 * (u_dir[0] * phi.cos() + v_dir[0] * phi.sin()),
cy + major_r * (u_dir[1] * phi.cos() + v_dir[1] * phi.sin()),
cz + major_r * (u_dir[2] * phi.cos() + v_dir[2] * phi.sin()),
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.
[
ring[0] + minor_r * (radial[0] * theta.cos() + axis[0] * theta.sin()),
+121 -215
View File
@@ -800,58 +800,53 @@ pub(crate) fn tessellate_entity(
// re-apply the table's scale factor on top of already-baked geometry.
// When the block exists we render it directly. Same pattern as
// Dimension's `block_name`.
if let EntityType::Table(tab) = e {
if let Some(br_h) = tab.block_record_handle {
if let Some(br) = document.block_records.iter().find(|br| br.handle == br_h) {
if !br.entity_handles.is_empty() {
let mut wires: Vec<WireModel> = Vec::with_capacity(br.entity_handles.len());
// The Table's own layer style — layer-0 inheritance target
// for baked sub-entities on layer "0" (#221).
let tab_l0_color = view::render::adapt_to_bg(
view::render::layer_render_style(document, &e.common().layer).color,
bg_color,
if let EntityType::Table(table) = e {
if let Some(record) = table.block_record_handle.and_then(|handle| {
document
.block_records
.iter()
.find(|record| record.handle == handle)
}) {
if !record.entity_handles.is_empty() {
let mut insert =
acadrust::entities::Insert::new(
record.name.clone(),
table.insertion_point,
);
let tab_l0_aci = document
.layers
.get(&e.common().layer)
.map(|l| match &l.color {
acadrust::types::Color::Index(i) => *i,
_ => 0,
})
.unwrap_or(0);
// The *T block is laid out in block-local space (origin at
// the table's top-left corner); the Table entity carries the
// world placement in `insertion_point` + `horizontal_direction`
// (like an INSERT). Place each baked sub-entity there before
// tessellating — without it the whole table renders at the
// origin. Rotating about the local origin then translating
// gives world = insertion + R·local; tessellate_entity then
// handles the UTM-scale relative-to-eye split as usual.
let ins = tab.insertion_point;
let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x);
for &eh in &br.entity_handles {
let Some(sub) = document.get_entity(eh) else {
continue;
};
insert.rotation = table
.horizontal_direction
.y
.atan2(table.horizontal_direction.x);
insert.common = table.common.clone();
let depths = rustc_hash::FxHashMap::default();
let graph = crate::scene::render_graph::RenderSceneGraph::new(
document,
None,
annotation_scale_handle,
true,
&depths,
);
let mut wires = Vec::new();
graph.walk_insert(
&insert,
h,
|_, _| true,
|sub, context| {
let has_book_color =
view::render::has_resolved_book_color(document, sub);
let sub_color_is_byblock = !has_book_color
&& sub.common().color == acadrust::types::Color::ByBlock;
let sub_is_l0_bylayer =
!has_book_color
&& view::render::is_effective_layer_zero(&sub.common().layer)
&& sub.common().color == acadrust::types::Color::ByLayer;
let color_byblock = !has_book_color
&& sub.common().color
== acadrust::types::Color::ByBlock;
let color_layer0 = !has_book_color
&& view::render::is_effective_layer_zero(
&sub.common().layer,
)
&& sub.common().color
== acadrust::types::Color::ByLayer;
let style = context.style_for(document, sub);
let mut placed = sub.clone();
{
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);
}
placed.apply_transform(&context.transform);
let sub_wires = tessellate_entity(
document,
selected,
@@ -865,43 +860,39 @@ pub(crate) fn tessellate_entity(
world_per_pixel,
paper_space,
);
for mut w in sub_wires {
w.name = h.value().to_string();
if sub_color_is_byblock {
w.color = if sel {
for mut wire in sub_wires {
wire.name = h.value().to_string();
if color_byblock {
wire.color = if sel {
WireModel::SELECTED
} else {
entity_color
view::render::adapt_to_bg(style.0, bg_color)
};
w.aci = aci;
} else if sub_is_l0_bylayer && !sel {
w.color = tab_l0_color;
w.aci = tab_l0_aci;
wire.aci = style.4;
} else if color_layer0 && !sel {
wire.color =
view::render::adapt_to_bg(style.0, bg_color);
wire.aci = style.4;
}
wires.push(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() {
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;
}
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 mut wires = crate::entities::table::tessellate_table(
tab,
table,
document,
sel,
entity_color,
@@ -909,7 +900,7 @@ pub(crate) fn tessellate_entity(
table_anno,
);
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(
document,
@@ -927,13 +918,10 @@ pub(crate) fn tessellate_entity(
}
if !wires.is_empty() {
let aabb = entity_aabb(e);
for w in &mut wires {
w.aci = aci;
// Empty SDF-text cells keep their tight glyph-box AABB; only
// stroke/fill wires take the whole-table box as a broad-phase
// pick hint (matches the dim / mleader / baked-block paths).
if !w.points.is_empty() || !w.fill_tris.is_empty() {
set_wire_aabb(w, aabb);
for wire in &mut wires {
wire.aci = aci;
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
set_wire_aabb(wire, aabb);
}
}
return wires;
@@ -986,145 +974,63 @@ pub(crate) fn tessellate_entity(
fill_tris_low: Vec::new(),
};
if let Some(cache) = block_cache {
// Xrefs render with the same hue but faded toward `bg_color` so
// the user can recognise external-reference geometry at a glance.
let is_xref = document
.block_records
.get(&ins.block_name)
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.unwrap_or(false);
if let Some(mut wires) = 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,
let fallback_depths = rustc_hash::FxHashMap::default();
let fallback_cache;
let cache = match block_cache {
Some(cache) if cache.defn(&ins.block_name).is_some() => cache,
_ => {
fallback_cache = cache::block_cache::BlockCache::build_for_block(
document,
ins,
h,
sel,
ins_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
bg_color,
is_xref,
pslt_factor,
&ins.block_name,
anno_scale,
annotation_scale_handle,
true,
bg_color,
&fallback_depths,
);
wires.push(marker);
return wires;
&fallback_cache
}
};
let is_xref = document
.block_records
.get(&ins.block_name)
.map(|record| record.flags.is_xref || record.flags.is_xref_overlay)
.unwrap_or(false);
let mut wires = cache::block_cache::expand_insert(
cache,
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(
&mut wires,
document,
+93 -64
View File
@@ -1547,6 +1547,15 @@ pub(crate) fn arrow_from_block(
doc: &CadDocument,
handle: acadrust::types::Handle,
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 {
if handle.is_null() {
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) {
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))
}
@@ -1653,6 +1662,7 @@ fn custom_arrow_from_block(
doc: &CadDocument,
record: &acadrust::tables::BlockRecord,
dimasz: f32,
defer_hatch: bool,
) -> Option<ArrowKind> {
if record.is_layout()
|| record.is_model_space()
@@ -1663,17 +1673,42 @@ fn custom_arrow_from_block(
{
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 fill = Vec::new();
let mut stack = vec![record.name.clone()];
for &entity_handle in &record.entity_handles {
let Some(entity) = doc.get_entity(entity_handle) else {
continue;
};
collect_custom_arrow_entity(doc, entity, base, &mut lines, &mut fill, &mut stack, 0);
}
if lines.is_empty() && fill.is_empty() {
let mut deferred_hatch = false;
graph.walk_insert(
&insert,
record.handle,
|_, _| true,
|entity, context| {
if defer_hatch && matches!(entity, EntityType::Hatch(_)) {
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
} else {
Some(ArrowKind::Custom {
@@ -1684,67 +1719,27 @@ fn custom_arrow_from_block(
}
}
fn block_base_point(
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(
fn append_custom_arrow_leaf(
doc: &CadDocument,
entity: &EntityType,
root_base: acadrust::types::Vector3,
lines: &mut Vec<[f32; 3]>,
fill: &mut Vec<[f32; 3]>,
stack: &mut Vec<String>,
depth: usize,
) {
if depth >= 32 || entity.common().invisible {
return;
}
match entity {
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_)
| EntityType::Dimension(_)
| EntityType::Leader(_)
| EntityType::MultiLeader(_) => return,
EntityType::Insert(insert) => {
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::MultiLeader(_)
| EntityType::Insert(_) => return,
EntityType::Hatch(hatch) => {
append_custom_hatch_fill(hatch, root_base, fill);
append_custom_hatch_geometry(
hatch,
acadrust::types::Vector3::ZERO,
lines,
fill,
);
return;
}
_ => {}
@@ -1766,15 +1761,26 @@ fn collect_custom_arrow_entity(
true,
);
for wire in wires {
append_custom_wire_points(&wire.points, &wire.points_low, root_base, lines);
append_custom_fill_points(&wire.fill_tris, &wire.fill_tris_low, root_base, fill);
append_custom_wire_points(
&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,
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::path::Path;
@@ -1786,6 +1792,29 @@ fn append_custom_hatch_fill(
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 ring: Vec<[f32; 2]> = Vec::new();
let mut finish_ring = |ring: &mut Vec<[f32; 2]>| {
@@ -1825,7 +1854,7 @@ fn append_custom_hatch_fill(
return;
}
let z = -base.z as f32;
out.extend(
fill.extend(
geometry
.indices
.iter()
+185 -628
View File
@@ -853,8 +853,7 @@ impl Scene {
}
// Reject block-defn-only hatches (entities owned by a
// BLOCK record that's neither model nor a paper layout
// block) — they're tessellated separately via Insert
// explosion and only the laid-out copies should appear.
// block) — the scene graph emits only their laid-out copies.
self.belongs_to_visible_block(handle, c.owner_handle, target_block)
})
.flat_map(|(&handle, model)| {
@@ -976,9 +975,8 @@ impl Scene {
} else {
self.bg_color
};
// Block-internal hatch fills: descend INSERTs owned by the requested
// target only and materialize their fills at world position.
models.extend(self.exploded_insert_hatch_models(
// Instanced/owned hatch leaves are produced by the shared scene graph.
models.extend(self.instanced_hatch_models(
target_block,
hatch_bg,
true,
@@ -987,31 +985,23 @@ impl Scene {
all_visible,
));
// Wide LwPolyline / Polyline2D bands are no longer hatch fills at
// model level: a flat band is drawn by expanding its centre-line wire
// 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.
// Wide polyline bands remain on the wire path, including inside block
// instances; the graph does not reclassify them as hatch fills.
models
}
/// Materialize the hatch fills that live *inside* the visible INSERTs of
/// `layout_block`, baking each INSERT's transform (and nested-INSERT
/// transforms) so the fills land in world position with resolved
/// ByBlock / layer-0 colours.
/// Materialize hatch leaves reached through visible scene containers in
/// `layout_block`, with transforms and inherited styles already resolved.
///
/// Shared by the on-screen hatch set (`synced_hatch_models`) and the
/// paper/export hatch set (`paper_canvas_hatches`) so a plot draws
/// block-internal hatches identically to the viewport. Without this the
/// export path only saw top-level hatches, so any fill nested in a block
/// INSERT printed as bare monochrome outlines.
/// block-internal hatches identically to the viewport.
///
/// `hatch_bg` adapts pure black/white leaf colours to the target
/// background; `tint_selected` re-colours fills of a selected INSERT
/// (screen highlight) and should be `false` for export.
pub(super) fn exploded_insert_hatch_models(
pub(super) fn instanced_hatch_models(
&self,
layout_block: Handle,
hatch_bg: [f32; 4],
@@ -1020,7 +1010,7 @@ impl Scene {
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) -> Vec<HatchModel> {
self.exploded_insert_hatch_models_filtered(
self.instanced_hatch_models_filtered(
layout_block,
hatch_bg,
tint_selected,
@@ -1058,7 +1048,7 @@ impl Scene {
.flatten()
.copied()
.collect();
self.exploded_insert_hatch_models_filtered(
self.instanced_hatch_models_filtered(
self.interaction_block_handle(),
hatch_bg,
true,
@@ -1070,7 +1060,7 @@ impl Scene {
)
}
fn exploded_insert_hatch_models_filtered(
fn instanced_hatch_models_filtered(
&self,
layout_block: Handle,
hatch_bg: [f32; 4],
@@ -1081,400 +1071,105 @@ impl Scene {
targets: Option<&rustc_hash::FxHashSet<Handle>>,
include_preview_hidden: bool,
) -> 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 block_count = |name: &str| -> usize {
self.document
.block_records
.get(name)
.map(|br| br.entity_handles.len())
.unwrap_or(1)
.max(1)
};
// Exploding an INSERT materializes (clones) every child of its block —
// including 3D solids that each carry megabytes of SAB geometry — just
// to scan the result for hatch fills. On xref-heavy drawings whose
// blocks hold thousands of solids and NO hatches, that cloned ~all block
// content on every open for nothing (~20 s on a 5-xref BIM aggregate).
// Skip the explode for blocks with no hatch (transitively) via the same
// 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 graph = crate::scene::render_graph::RenderSceneGraph::new(
&self.document,
frozen,
annotation_scale_handle,
all_visible,
depth_map.as_ref(),
);
let mut hatch_block_memo = std::collections::HashMap::new();
let mut models = Vec::new();
graph.walk_root(
self.render_scene_root(layout_block),
|entity, context| {
if context.is_instanced() {
return true;
}
let ins_pt = dim.base().insertion_point;
let Some(dblk) = doc
.block_records
.iter()
.find(|r| r.name.eq_ignore_ascii_case(&bn))
else {
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);
let common = entity.common();
if self.object_isolation.hides(common.handle)
|| (!include_preview_hidden
&& self.preview_hidden.contains(&common.handle))
{
return false;
}
}
out
}
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;
};
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(
if let Some(targets) = targets {
return matches!(entity, EntityType::Insert(_))
&& targets.contains(&common.handle);
}
match entity {
EntityType::Insert(insert) => {
crate::scene::render_graph::block_contains_hatch(
&self.document,
&hatch_entity,
sub_ins_style.0,
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);
}
&insert.block_name,
&mut hatch_block_memo,
)
}
// Wide LwPolyline / Polyline2D bands inside a block are drawn
// by the shader wire path — block_cache carries each centre-
// line's `world_width` and scales it by the insert transform
// (`emit_wire`) — not as hatch fills here. Same depth-tested,
// linetype-dashed band as at model level. (#222)
_ => {}
EntityType::Dimension(dimension) => {
let name = dimension.base().block_name.trim();
!name.is_empty()
&& crate::scene::render_graph::block_contains_hatch(
&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
}
@@ -1487,245 +1182,107 @@ impl Scene {
annotation_scale_handle: Option<Handle>,
all_visible: bool,
) -> Vec<HatchModel> {
let is_paper = self.current_layout != "Model";
let bg_color: [f32; 4] = if is_paper {
let bg_color = if self.current_layout != "Model" {
self.paper_bg_color
} else {
self.bg_color
};
// No per-frame view-cull here: GPU wipeout buffer upload is
// gated on geometry_epoch only (see render.rs), so any cull at
// build time would freeze the visible subset at the geometry
// epoch boundary and never re-evaluate as the user pans. The
// pipeline's `wipeout_skip_flags` (compute_wipeout_lod) does
// the per-frame skip at draw time instead.
self.wipeout_models_for_block_graph(
target_block,
frozen,
annotation_scale_handle,
all_visible,
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 graph = crate::scene::render_graph::RenderSceneGraph::new(
&self.document,
frozen,
annotation_scale_handle,
all_visible,
depth_map.as_ref(),
);
let mut models = Vec::new();
for entity in self.document.entities() {
let EntityType::Wipeout(wo) = entity else {
continue;
};
if entity.common().invisible
|| self.entity_temporarily_hidden(wo.common.handle)
{
continue;
}
// Reject block-defn-only wipeouts (owned by a BLOCK record that is
// neither model nor a paper layout block): those are placed via
// insert descent below, in world space. Without this they also draw
// once at their raw block-local coordinates. Mirrors the hatch path.
if !self.belongs_to_visible_block(
wo.common.handle,
wo.common.owner_handle,
target_block,
) {
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];
graph.walk_root(
self.render_scene_root(target_block),
|entity, context| {
context.is_instanced()
|| !self.entity_temporarily_hidden(entity.common().handle)
},
|entity, context| {
let EntityType::Wipeout(source) = entity else {
return;
};
let mut wipeout = source.clone();
if context.is_instanced() {
wipeout.insertion_point =
context.transform.apply(source.insertion_point);
wipeout.u_vector =
context.transform.apply_rotation(source.u_vector);
wipeout.v_vector =
context.transform.apply_rotation(source.v_vector);
}
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 {
boundary: Arc::new(boundary),
boundary_wcs: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color: fill_color,
color,
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
scale: 1.0,
world_origin: fill_origin,
// The mask erases what draws below the wipeout and loses
// to what draws above — including its own frame wire,
// 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]),
world_origin,
draw_depth: context
.draw_depth(source.common.handle, depth_map.as_ref()),
});
}
}
// 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
}
/// 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.
/// Wipeout fill boundary as small f32 offsets from the returned `world_origin`
/// (the insertion point, kept in f64). Building it in absolute WCS f32
/// collapsed the boundary into squares at UTM-scale coordinates; the
/// relative-to-eye hatch fill reconstructs `world_origin + offset` precisely.
/// Wipeout fill boundary as small f32 offsets from the returned world_origin
/// (the insertion point, kept in f64).
pub(super) fn wipeout_boundary_2d(
wo: &acadrust::entities::Wipeout,
) -> ([f64; 2], Vec<[f32; 2]>) {
+238 -432
View File
@@ -12,6 +12,7 @@ pub mod creation_style;
pub mod model;
pub mod pick;
pub mod pipeline;
pub(crate) mod render_graph;
pub mod text;
pub mod view;
@@ -1699,11 +1700,9 @@ pub struct Scene {
RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<HatchModel>>)>>,
frozen_image_cache: RefCell<HashMap<(Handle, u64), (u64, Arc<Vec<ImageModel>>)>>,
frozen_mesh_cache: RefCell<HashMap<(Handle, String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch.
/// Building this explodes every model-space INSERT, so without the cache a
/// heavy block-instanced drawing re-explodes thousands of inserts on every
/// hover. The set is geometry-derived, so a camera move / hover never
/// invalidates it.
/// Cached block-instance hatches for hit-testing, keyed by geometry_epoch.
/// The set is geometry-derived, so a camera move or hover never invalidates
/// the shared graph result.
insert_hatch_cache: RefCell<Option<(u64, u64, Arc<HashMap<Handle, Vec<HatchModel>>>)>>,
/// Sheet "dressing" cache over the unified resident set: the paper sheet
/// drops its own border wire and appends the printable-area guide.
@@ -5772,6 +5771,7 @@ impl Scene {
None,
scale,
self.annotation_all_visible(),
None,
));
self.image_cache
.borrow_mut()
@@ -5787,164 +5787,82 @@ impl Scene {
frozen: Option<&HashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
all_visible: bool,
viewport: Option<Handle>,
) -> Vec<ImageModel> {
let depth_map = self.draw_depth_map();
let mut models: Vec<ImageModel> = self.images
.iter()
.filter_map(|(handle, model)| {
let entity = self.document.get_entity(*handle)?;
let common = entity.common();
if common.invisible
|| self.entity_temporarily_hidden(*handle)
|| self.layer_hidden(&common.layer)
|| self.layer_frozen_in(&common.layer, frozen)
|| !self.belongs_to_visible_block(
*handle,
common.owner_handle,
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)
let graph = render_graph::RenderSceneGraph::new(
&self.document,
frozen,
annotation_scale_handle,
all_visible,
depth_map.as_ref(),
);
let mut models = Vec::new();
let root = if self.block_edit_block.is_none() {
viewport.map(|viewport| render_graph::SceneRoot::Viewport {
paper_block: self.current_layout_block_handle(),
viewport,
model_block: target_block,
})
.collect();
for source in self.document.entities() {
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,
);
} else {
None
}
.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
}
#[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
/// sheet viewport uses this so model-block images don't bleed onto the
/// paper sheet (mirrors `paper_canvas_hatches`).
@@ -5955,6 +5873,7 @@ impl Scene {
None,
self.paper_annotation_scale_handle(),
self.annotation_all_visible(),
None,
))
}
@@ -6020,6 +5939,20 @@ impl Scene {
.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 {
if self.block_edit_block.is_none() && self.active_viewport.is_some() {
self.model_space_block_handle()
@@ -6318,6 +6251,7 @@ impl Scene {
Some(frozen),
scale,
all_visible,
Some(viewport),
));
self.frozen_image_cache
.borrow_mut()
@@ -6434,184 +6368,110 @@ impl Scene {
if self.block_meshes.is_empty() {
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();
for source in self.document.entities() {
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
source,
annotation_scale_handle,
);
let e = contextual.as_ref();
if e.common().owner_handle != layout_block {
continue;
}
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;
graph.walk_root(
self.render_scene_root(layout_block),
|entity, context| {
context.is_instanced()
|| !self.entity_temporarily_hidden(entity.common().handle)
},
|entity, context| {
if !context.is_instanced() {
return;
}
// Colour inheritance sources for block-internal solids (#221).
let bg = self.current_bg();
let ins_color = crate::scene::view::render::adapt_to_bg(
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 handle = entity.common().handle;
let Some(set) = self.block_meshes.get(&handle) else {
return;
};
let start = out.len();
self.expand_block_meshes(
&ins.block_name,
&ins.get_transform(),
0,
Some(inherit),
&mut out,
frozen,
annotation_scale_handle,
all_visible,
);
// Tag the instanced meshes with the parent INSERT handle so the
// hover / selection highlight (keyed on the mesh name) tints the
// 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();
let inherit = self.mesh_inherit_for_path(&context.insert_path);
let own_alpha =
set.lods.first().map(|mesh| mesh.color[3]).unwrap_or(1.0);
let mut transformed =
transform_block_mesh_lod_set(set, &context.transform);
if let Some(color) = self.block_mesh_override_color(
entity,
handle,
inherit.as_ref(),
own_alpha,
) {
for mesh in &mut transformed.lods {
mesh.color = color;
}
}
}
}
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 let Some(material) = transformed.material.as_mut() {
if material.handle.is_none() {
material.diffuse = c;
material.diffuse = color;
}
}
}
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(
&mut ts,
&mut transformed,
&self.document,
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).
@@ -6811,93 +6671,7 @@ impl Scene {
/// 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
/// Insert (AutoCAD behaviour: sub-entities of a block aren't directly
/// 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
}
/// Insert; block-owned leaves resolve to their owning instance.
pub fn insert_hatches_for_click(&self) -> Arc<HashMap<Handle, Vec<HatchModel>>> {
let interaction_block = self.interaction_block_handle();
@@ -6931,13 +6705,26 @@ impl Scene {
let layer_hidden =
|layer: &str| self.layer_hidden(layer) || self.interaction_layer_frozen(layer);
let mut out: HashMap<Handle, Vec<HatchModel>> = HashMap::default();
// Exploding an INSERT to find block-internal hatches is expensive, so
// skip blocks that contain no hatch at all (the common case for solid-
// only blocks). The hatch-presence test is memoised across inserts.
// Skip block subtrees that contain no hatch at all. The presence test
// is memoised across instances.
let mut hatch_memo: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
let annotation_scale_handle = self.displayed_annotation_scale_handle();
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() {
let contextual = crate::scene::annotative::entity_for_annotation_context(
&self.document,
@@ -6966,33 +6753,52 @@ impl Scene {
) {
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;
}
for sub in ins
.explode_from_document(&self.document)
.into_iter()
.map(|sub| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
&sub,
annotation_scale_handle,
)
.into_owned()
})
.map(crate::modules::draw::modify::explode::normalize_insert_entity)
{
let EntityType::Hatch(dxf) = sub else {
continue;
};
if dxf.common.invisible || layer_hidden(&dxf.common.layer) {
continue;
}
let color = self.render_style(&EntityType::Hatch(dxf.clone())).0;
if let Some(model) = Self::hatch_model_from_dxf(&dxf, color) {
graph.walk_insert(
ins,
ins.common.handle,
|_, _| true,
|entity, context| {
let EntityType::Hatch(source) = entity else {
return;
};
let mut placed = EntityType::Hatch(source.clone());
placed.apply_transform(&context.transform);
let EntityType::Hatch(hatch) = placed else {
return;
};
let color = crate::scene::view::render::adapt_to_bg(
context.style_for(&self.document, entity).0,
self.current_bg(),
);
let Some(mut model) = Self::hatch_model_from_dxf(&hatch, color)
else {
return;
};
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);
}
}
},
);
}
let arc = Arc::new(out);
*self.insert_hatch_cache.borrow_mut() =
+6 -13
View File
@@ -124,7 +124,7 @@ impl MeshMaterial {
description: String::new(),
diffuse: color,
ambient: [color[0], color[1], color[2]],
specular: [1.0; 3],
specular: [0.08; 3],
gloss: 0.5,
reflectivity: 0.0,
self_illumination: 0.0,
@@ -169,18 +169,11 @@ impl MeshMaterial {
let bump_map = MeshTextureMap::from_dwg(&material.bump_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);
// A material whose diffuse channel explicitly references an external
// texture is incomplete when that file cannot be resolved. Its stored
// diffuse RGB is commonly only a red/magenta placeholder (for example
// VILLA_COMPLETE's Plaster material), not the intended appearance.
// Keep the material's other physical properties, but use the entity's
// 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)
};
// The stored diffuse component remains authoritative when an external
// image is unavailable. Replacing it with the entity colour would lose
// a decoded material value and make the result depend on a guess about
// the missing asset's intended appearance.
let diffuse = material_color(&material.diffuse_color, entity_color);
let ambient = material_color(&material.ambient_color, entity_color);
let specular = material_color(&material.specular_color, [1.0; 4]);
let opacity = material.opacity_percent.clamp(0.0, 1.0) as f32;
+5 -2
View File
@@ -84,8 +84,8 @@ pub enum CurvedGen {
phi_min: f32,
phi_max: f32,
},
/// Torus: the two extreme circles of the tube (outer/inner), clipped to the
/// revolution window the face covers.
/// Torus: view-dependent tube silhouette, clipped to both parametric
/// windows the face covers.
Torus {
center: [f32; 3],
center_low: [f32; 3],
@@ -97,6 +97,9 @@ pub enum CurvedGen {
phi_min: f32,
phi_span: f32,
full: bool,
theta_min: f32,
theta_span: f32,
theta_full: bool,
},
}
+16 -99
View File
@@ -462,16 +462,15 @@ impl Scene {
}
// Hatch fills nested inside a block INSERT are owned by the block
// record, so the loop above — which only keeps hatches owned by
// `layout_block` — never sees them. Explode the layout's visible
// INSERTs and materialize their fills at world position, exactly as the
// viewport does, so the export carries the block's colours instead of
// bare outlines. (No selection tint on export.)
// `layout_block` — never sees them. Walk the layout's visible block
// instances and materialize their fills at world position, so export
// carries the block's colours instead of bare outlines.
let hatch_bg = if self.current_layout != "Model" {
self.paper_bg_color
} else {
self.bg_color
};
let exploded = self.exploded_insert_hatch_models(
let instanced = self.instanced_hatch_models(
layout_block,
hatch_bg,
false,
@@ -479,7 +478,7 @@ impl Scene {
annotation_scale_handle,
all_visible,
);
models.extend(exploded);
models.extend(instanced);
Arc::new(models)
}
@@ -554,7 +553,7 @@ impl Scene {
}
models.push(hatch);
}
models.extend(self.exploded_insert_hatch_models(
models.extend(self.instanced_hatch_models(
block,
self.paper_bg_color,
false,
@@ -565,8 +564,8 @@ impl Scene {
models
}
/// Plot-only wipeout masks for a specific block. Nested inserts follow the
/// same recursive collector as the on-screen model path.
/// Plot-only wipeout masks for a specific block. Nested instances follow
/// the same scene graph as the on-screen model path.
pub(super) fn plot_wipeouts_for_block(
&self,
block: Handle,
@@ -575,96 +574,14 @@ impl Scene {
all_visible: bool,
highlight_selection: bool,
) -> Vec<HatchModel> {
let depth_map = self.draw_depth_map();
let mut models = Vec::new();
for entity in self.document.entities() {
let EntityType::Wipeout(wipeout) = entity else {
continue;
};
let common = &wipeout.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)
|| !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
self.wipeout_models_for_block_graph(
block,
frozen,
annotation_scale_handle,
all_visible,
self.paper_bg_color,
highlight_selection,
)
}
/// Paper-layout wipeout fills (paper hit-testing / export). Same rationale as
+20 -22
View File
@@ -12,7 +12,7 @@
use acadrust::entities::Insert;
use acadrust::objects::{ObjectType, SpatialFilter};
use acadrust::types::{Handle, Vector3};
use acadrust::types::{Handle, Transform, Vector3};
use acadrust::CadDocument;
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.)
/// Two points describe a rectangle (opposite corners); three or more an
/// explicit polygon.
pub fn world_clip_polygon(
sf: &SpatialFilter,
ins: &Insert,
) -> Vec<[f32; 2]> {
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(
/// Build the clip boundary in absolute f64 world coordinates so clipping at
/// large coordinate values remains precise.
#[cfg(test)]
fn world_clip_polygon_f64(
sf: &SpatialFilter,
ins: &Insert,
) -> 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 local: Vec<[f64; 2]> = if sf.boundary_points.len() == 2 {
let a = sf.boundary_points[0];
@@ -764,13 +762,13 @@ mod tests {
ins.set_x_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 →
// (639668, 4516955).
let xs: Vec<f32> = poly.iter().map(|p| p[0]).collect();
let ys: Vec<f32> = poly.iter().map(|p| p[1]).collect();
let minx = xs.iter().cloned().fold(f32::MAX, f32::min);
let miny = ys.iter().cloned().fold(f32::MAX, f32::min);
let xs: Vec<f64> = poly.iter().map(|p| p[0]).collect();
let ys: Vec<f64> = poly.iter().map(|p| p[1]).collect();
let minx = xs.iter().cloned().fold(f64::MAX, f64::min);
let miny = ys.iter().cloned().fold(f64::MAX, f64::min);
assert!((minx - 639668.0).abs() < 1.0, "minx={minx}");
assert!((miny - 4516955.0).abs() < 1.0, "miny={miny}");
}
+227 -65
View File
@@ -406,6 +406,16 @@ struct MaterialMapParams {
blends1: [f32; 4],
/// Presence bits for the same four maps.
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(
@@ -490,7 +500,7 @@ pub fn create_material_bind_group(
);
let sampler = |label: &'static str, tiling: u8| {
let address = match tiling {
1 => wgpu::AddressMode::Repeat,
0 | 1 => wgpu::AddressMode::Repeat,
4 => wgpu::AddressMode::MirrorRepeat,
_ => wgpu::AddressMode::ClampToEdge,
};
@@ -525,6 +535,21 @@ pub fn create_material_bind_group(
let bump_sampler = sampler("mesh.material.bump_sampler", tiling(4));
let refraction_sampler = sampler("mesh.material.refraction_sampler", tiling(5));
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 {
blends0: material.map_or([0.0; 4], |material| {
[
@@ -534,12 +559,12 @@ pub fn create_material_bind_group(
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,
material.specular_map.image.is_some() as u32,
material.reflection_map.image.is_some() as u32,
material.opacity_map.image.is_some() as u32,
channel_present(0, 0x01),
channel_present(1, 0x02),
channel_present(2, 0x04),
channel_present(3, 0x08),
]
}),
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| {
[
material.bump_map.image.is_some() as u32,
material.refraction_map.image.is_some() as u32,
material.normal_map.image.is_some() as u32,
channel_present(4, 0x10),
channel_present(5, 0x20),
material.normal_map.image.is_some() as u32
* (material.normal_map_method == 0) as u32,
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 {
label: Some("mesh.material.params"),
@@ -656,51 +717,45 @@ pub fn create_material_bind_group(
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct MaterialBatchKey([u32; 16]);
struct MaterialBatchKey([u32; 40]);
fn material_key(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
) -> MaterialBatchKey {
let mut key = [0u32; 40];
key[2..6].copy_from_slice(&color.map(f32::to_bits));
let Some(material) = material else {
return MaterialBatchKey([
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,
]);
return MaterialBatchKey(key);
};
let handle = material.handle.map_or(0, |handle| handle.value());
MaterialBatchKey([
handle as u32,
(handle >> 32) as u32,
material.diffuse[0].to_bits(),
material.diffuse[1].to_bits(),
material.diffuse[2].to_bits(),
material.diffuse[3].to_bits(),
material.specular[0].to_bits(),
material.specular[1].to_bits(),
material.specular[2].to_bits(),
material.gloss.to_bits(),
material.reflectivity.to_bits(),
material.self_illumination.to_bits(),
material.luminance.to_bits(),
material.refraction_index.to_bits(),
material.diffuse_map.projection as u32,
material.diffuse_map.tiling as u32,
])
key[0] = handle as u32;
key[1] = (handle >> 32) as u32;
key[6..10].copy_from_slice(&material.diffuse.map(f32::to_bits));
key[10..13].copy_from_slice(&material.ambient.map(f32::to_bits));
key[13..16].copy_from_slice(&material.specular.map(f32::to_bits));
key[16] = material.gloss.to_bits();
key[17] = material.reflectivity.to_bits();
key[18] = material.self_illumination.to_bits();
key[19] = material.translucence.to_bits();
key[20] = material.refraction_index.to_bits();
key[21] = material.luminance.to_bits();
key[22] = material.two_sided as u32;
key[23] = material.illumination_model as u32;
key[24] = material.channel_flags as u32;
key[25] = material.mode as u32;
key[26] = material.indirect_bump_scale.to_bits();
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 {
@@ -736,6 +791,7 @@ struct MeshBatchPart<'a> {
set: &'a MeshLodSet,
mesh: &'a MeshModel,
display_mesh: &'a MeshModel,
uv_mesh: &'a MeshModel,
material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
indices: Vec<u32>,
@@ -772,6 +828,7 @@ fn build_instanced_chunk(
let material = first.material;
let color = first.color;
let has_normals = mesh.normals.len() == mesh.verts.len();
let bounds = mesh_bounds(mesh);
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let vertex = |index: usize, edge: bool| {
@@ -799,7 +856,15 @@ fn build_instanced_chunk(
let uvs = if edge {
[[0.0; 2]; 7]
} else {
material_uvs(material, position, normal)
material_uvs(
material,
position,
normal,
bounds,
position,
normal,
bounds,
)
};
MeshVertex {
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 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(
map: &crate::scene::model::material_model::MeshTextureMap,
position: [f32; 3],
normal: [f32; 3],
local_position: [f32; 3],
local_normal: [f32; 3],
local_bounds: [f32; 6],
model_position: [f32; 3],
model_normal: [f32; 3],
model_bounds: [f32; 6],
) -> [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 p = [
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(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
position: [f32; 3],
normal: [f32; 3],
local_position: [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] {
let Some(material) = material else {
return [[0.0; 2]; 7];
};
[
material_map_uv(&material.diffuse_map, position, normal),
material_map_uv(&material.specular_map, position, normal),
material_map_uv(&material.reflection_map, position, normal),
material_map_uv(&material.opacity_map, position, normal),
material_map_uv(&material.bump_map, position, normal),
material_map_uv(&material.refraction_map, position, normal),
material_map_uv(&material.normal_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, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.reflection_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.opacity_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.bump_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
material_map_uv(&material.refraction_map, local_position, local_normal, local_bounds, model_position, model_normal, model_bounds),
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 {
return (
[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],
[1.0; 4],
[0, 127, 0, 0],
@@ -1089,11 +1227,11 @@ pub fn build_mesh_batch_filtered(
}) {
continue;
}
let mesh = set
let source_mesh = set
.instance_source
.as_ref()
.and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty()))
.unwrap_or(display_mesh);
.and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty()));
let mesh = source_mesh.unwrap_or(display_mesh);
let triangle_count = display_mesh.indices.len() / 3;
let has_face_materials =
display_mesh.triangle_material_handles.len() == triangle_count
@@ -1119,6 +1257,7 @@ pub fn build_mesh_batch_filtered(
set,
mesh,
display_mesh,
uv_mesh: mesh,
material: set.material.as_ref(),
color,
indices: display_mesh.indices.clone(),
@@ -1166,6 +1305,7 @@ pub fn build_mesh_batch_filtered(
set,
mesh,
display_mesh,
uv_mesh: mesh,
material,
color,
indices,
@@ -1211,6 +1351,7 @@ pub fn build_mesh_batch_filtered(
let eligible = storage_instancing
&& part.set.instance_transform.is_some()
&& part.set.instance_source.is_some()
&& !material_uses_model_mapper(part.material)
&& part.mesh.verts.len() <= max_verts
&& part.indices.len() / 3 <= max_tris
&& part
@@ -1336,6 +1477,9 @@ pub fn build_mesh_batch_filtered(
let has_normals = mesh.normals.len() == mesh.verts.len();
let (material_params, specular, ambient, advanced, flags) =
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
.visual_style
.as_ref()
@@ -1346,7 +1490,25 @@ pub fn build_mesh_batch_filtered(
} else {
[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 {
position: mesh.verts[vi],
normal,
@@ -1368,7 +1530,7 @@ pub fn build_mesh_batch_filtered(
};
// A solid whose baked colour is not fully opaque routes into the
// 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;
if part.include_faces {
total_tris += mesh_tris as u64;
+12 -7
View File
@@ -2109,7 +2109,7 @@ impl Pipeline {
}
CurvedGen::Torus {
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 (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];
for k in 0..2 {
let t = cur[k];
let p = ring + (radial * t.cos() + axis * t.sin()) * minor;
if let Some(pp) = prev[k] {
let theta_offset =
(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(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());
// Fill buffer split: 3D quads + PolyfaceMesh / PolygonMesh face
// 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
// planar SOLID interiors; HATCH is handled by a separate pass.
let keep_3d_mesh_fills = !wireframe_only;
@@ -2289,11 +2294,11 @@ impl Pipeline {
};
let has_any_2d_fill = all_wires
.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()
|| all_wires
.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);
if !has_fills {
self.gpu_face3d_fill = None;
+709
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
}
+81 -24
View File
@@ -37,6 +37,11 @@ struct MaterialMaps {
present0: vec4<u32>,
blends1: vec4<f32>,
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>;
@@ -186,8 +191,20 @@ fn vs_edge(
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
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let two_sided = maps.render_modes.x != 0u;
var n: vec3<f32>;
if (u.flat_shade > 0.5) {
// Per-triangle face normal: derivatives of the interpolated
@@ -197,7 +214,6 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
} else {
n = normalize(in.normal);
}
// Build a derivative tangent frame from the generated material UVs. This
// works for planar/box/cylindrical/spherical AcDbMaterial projections and
// 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(
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)));
} else if (maps.present1.x != 0u) {
let height = dot(
@@ -226,7 +244,12 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
vec3<f32>(0.2126, 0.7152, 0.0722),
);
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,
@@ -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 l2 = normalize(vec3<f32>( 0.2, -0.6, -0.8)); // back/under
var direct_light = vec3<f32>(
0.45 * abs(dot(n, l0))
+ 0.30 * abs(dot(n, l1))
+ 0.25 * abs(dot(n, l2))
0.45 * select(max(dot(n, l0), 0.0), abs(dot(n, l0)), two_sided)
+ 0.30 * select(max(dot(n, l1), 0.0), abs(dot(n, l1)), two_sided)
+ 0.25 * select(max(dot(n, l2), 0.0), abs(dot(n, l2)), two_sided)
);
var key_light = l0;
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;
direct_light += color_hotspot.rgb
* max(dot(n, light_vector), 0.0)
* strength;
let response = select(
max(dot(n, light_vector), 0.0),
abs(dot(n, light_vector)),
two_sided,
);
direct_light += color_hotspot.rgb * response * strength;
if (index == 0u) {
key_light = light_vector;
}
@@ -298,26 +324,36 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
);
let fresnel = fresnel0
+ (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);
var specular_color = in.specular.rgb;
let specular_strength = clamp(1.0 + in.material.y + fresnel, 0.0, 1.5);
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) {
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(
abs(dot(n, half_vec)),
max(dot(n, half_vec), 0.0),
u.lighting.x > 0.5,
abs(dot(n, half_vec)),
two_sided,
);
let specular = specular_color
* pow(half_response, gloss_exp)
* specular_strength
* 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));
var lit = ambient_light + albedo * clamp(direct_light, vec3<f32>(0.0), vec3<f32>(2.0)) + specular;
if (maps.present0.z != 0u) {
@@ -329,7 +365,14 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
lit = mix(
lit,
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) {
@@ -341,15 +384,29 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
lit = mix(
lit,
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);
let rgb = mix(lit, albedo, emission);
var rgb = mix(lit, albedo, clamp(in.material.z, 0.0, 1.0));
if (in.flags.w == 1u) {
rgb = lit + albedo * max(in.material.w, 0.0);
}
var material_alpha = in.color.a;
if (maps.present0.w != 0u) {
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);
let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5);