feat(scene): integrate standard DWG data

This commit is contained in:
Hakan Seven 2026-07-26 22:30:14 +03:00
commit eb23715605
38 changed files with 8365 additions and 607 deletions

View file

@ -8,10 +8,11 @@
use acadrust::entities::{EntityCommon, EntityType};
use acadrust::objects::{
Dictionary, MTextContext, ObjectContextData, ObjectContextKind, ObjectType,
Dictionary, HatchScaleContext, MTextContext, ObjectContextData, ObjectContextKind, ObjectType,
};
use acadrust::types::{Vector2, Vector3};
use acadrust::{CadDocument, Handle};
use std::borrow::Cow;
/// Resolve a handle to a `Dictionary` object, if it is one.
pub fn as_dict(doc: &CadDocument, handle: Handle) -> Option<&Dictionary> {
@ -344,6 +345,566 @@ fn annotation_scales_dict(doc: &CadDocument, entity: Handle) -> Option<Handle> {
as_dict(doc, mgr).and_then(|d| d.get("ACDB_ANNOTATIONSCALES"))
}
/// Resolve the representation leaf used by the drawing's current annotation
/// scale. Broken scale handles are ignored. When the current named scale is
/// absent, the leaf explicitly marked as the native/default representation is
/// preferred, followed by the first valid leaf.
pub fn active_object_context(
doc: &CadDocument,
entity: Handle,
) -> Option<&ObjectContextData> {
let coll_h = annotation_scales_dict(doc, entity)?;
let coll = as_dict(doc, coll_h)?;
let mut default = None;
let mut first = None;
for (_, leaf_h) in &coll.entries {
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get(leaf_h) else {
continue;
};
first.get_or_insert(leaf);
if leaf.is_default {
default = Some(leaf);
}
let Some(ObjectType::Scale(scale)) = doc.objects.get(&leaf.scale) else {
continue;
};
if scale
.name
.eq_ignore_ascii_case(&doc.header.current_annotation_scale)
{
return Some(leaf);
}
}
default.or(first)
}
fn text_horizontal(value: i16) -> acadrust::entities::TextHorizontalAlignment {
use acadrust::entities::TextHorizontalAlignment;
match value {
1 => TextHorizontalAlignment::Center,
2 => TextHorizontalAlignment::Right,
3 => TextHorizontalAlignment::Aligned,
4 => TextHorizontalAlignment::Middle,
5 => TextHorizontalAlignment::Fit,
_ => TextHorizontalAlignment::Left,
}
}
fn mtext_attachment(value: i32) -> acadrust::entities::AttachmentPoint {
use acadrust::entities::AttachmentPoint;
match value {
2 => AttachmentPoint::TopCenter,
3 => AttachmentPoint::TopRight,
4 => AttachmentPoint::MiddleLeft,
5 => AttachmentPoint::MiddleCenter,
6 => AttachmentPoint::MiddleRight,
7 => AttachmentPoint::BottomLeft,
8 => AttachmentPoint::BottomCenter,
9 => AttachmentPoint::BottomRight,
_ => AttachmentPoint::TopLeft,
}
}
fn apply_mtext_context(entity: &mut acadrust::entities::MText, context: &MTextContext) {
entity.attachment_point = mtext_attachment(context.attachment);
entity.insertion_point = context.insertion;
entity.rectangle_width = context.rect_width;
entity.rectangle_height = (context.rect_height > 0.0).then_some(context.rect_height);
entity.extents_width = context.extents_width;
entity.extents_height = context.extents_height;
entity.rotation = context.x_axis_dir.y.atan2(context.x_axis_dir.x);
entity.dwg_x_direction = Some(context.x_axis_dir);
if let Some(columns) = &context.columns {
entity.column_data.column_type = context.column_type as i16;
entity.column_data.column_count = columns.num_heights;
entity.column_data.width = columns.width;
entity.column_data.gutter = columns.gutter;
entity.column_data.auto_height = columns.auto_height;
entity.column_data.flow_reversed = columns.flow_reversed;
entity.column_data.heights.clone_from(&columns.heights);
} else {
entity.column_data.column_type = context.column_type as i16;
entity.column_data.column_count = 0;
entity.column_data.heights.clear();
}
}
fn apply_dimension_context(
dimension: &mut acadrust::entities::Dimension,
context: &acadrust::objects::DimContext,
doc: &CadDocument,
) {
use acadrust::entities::Dimension;
use acadrust::objects::DimSubtype;
{
let base = dimension.base_mut();
base.text_middle_point.x = context.def_pt.x;
base.text_middle_point.y = context.def_pt.y;
base.text_rotation = context.text_rotation;
base.text_user_positioned = context.is_def_textloc;
base.flip_arrow1 = context.flip_arrow1;
base.flip_arrow2 = context.flip_arrow2;
if let Some(record) = doc.block_records.iter().find(|r| r.handle == context.block) {
base.block_name.clone_from(&record.name);
}
}
match (&context.subtype, dimension) {
(DimSubtype::Aligned { dimline_pt }, Dimension::Aligned(dim)) => {
dim.definition_point = *dimline_pt;
dim.base.definition_point = *dimline_pt;
}
(DimSubtype::Aligned { dimline_pt }, Dimension::Linear(dim)) => {
dim.definition_point = *dimline_pt;
dim.base.definition_point = *dimline_pt;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular2Ln(dim)) => {
dim.dimension_arc = *arc_pt;
dim.base.definition_point = *arc_pt;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular3Pt(dim)) => {
dim.definition_point = *arc_pt;
dim.base.definition_point = *arc_pt;
}
(
DimSubtype::Diametric {
first_arc_pt,
def_pt,
},
Dimension::Diameter(dim),
) => {
dim.angle_vertex = *first_arc_pt;
dim.definition_point = *def_pt;
dim.base.definition_point = *def_pt;
}
(DimSubtype::Radial { first_arc_pt }, Dimension::Radius(dim)) => {
dim.definition_point = *first_arc_pt;
dim.base.definition_point = *first_arc_pt;
}
(
DimSubtype::RadialLarge {
ovr_center,
jog_point,
},
Dimension::LargeRadial(dim),
) => {
dim.override_center = *ovr_center;
dim.jog_point = *jog_point;
}
(
DimSubtype::Ordinate {
feature_location_pt,
leader_endpt,
},
Dimension::Ordinate(dim),
) => {
dim.feature_location = *feature_location_pt;
dim.leader_endpoint = *leader_endpt;
}
_ => {}
}
}
fn apply_attribute_context(
insertion_point: &mut Vector3,
alignment_point: &mut Vector3,
rotation: &mut f64,
horizontal_alignment: &mut acadrust::entities::HorizontalAlignment,
embedded_mtext: &mut Option<Box<acadrust::entities::MText>>,
context: &acadrust::objects::MTextAttributeContext,
) {
insertion_point.x = context.insertion.x;
insertion_point.y = context.insertion.y;
alignment_point.x = context.alignment.x;
alignment_point.y = context.alignment.y;
*rotation = context.rotation;
*horizontal_alignment =
acadrust::entities::HorizontalAlignment::from_value(context.horizontal_mode);
if context.enable_context {
if let (Some(embedded), Some(mtext)) = (&context.context, embedded_mtext.as_mut()) {
apply_mtext_context(mtext, &embedded.mtext);
}
}
}
fn apply_hatch_context(hatch: &mut acadrust::entities::Hatch, context: &HatchScaleContext) {
hatch.pattern.lines.clone_from(&context.pattern_lines);
hatch.pattern_scale = context.pattern_scale;
for line in &mut hatch.pattern.lines {
line.base_point.x += context.pattern_base.x;
line.base_point.y += context.pattern_base.y;
}
for (path, bits) in hatch.paths.iter_mut().zip(&context.loop_types) {
path.flags = acadrust::entities::BoundaryPathFlags::from_bits(*bits as u32);
}
}
/// Return an ephemeral entity representation with the active scale leaf
/// overlaid on its base geometry. The source document remains unchanged, which
/// keeps save/round-trip data intact while render, picking and block expansion
/// all see the scale-specific placement.
pub fn entity_for_active_context<'a>(
doc: &'a CadDocument,
entity: &'a EntityType,
) -> Cow<'a, EntityType> {
let Some(context) = active_object_context(doc, entity.common().handle) else {
return Cow::Borrowed(entity);
};
let mut placed = entity.clone();
match (&context.kind, &mut placed) {
(
ObjectContextKind::BlkRef {
rotation,
insertion,
scale_factor,
},
EntityType::Insert(insert),
) => {
insert.rotation = *rotation;
insert.insert_point = *insertion;
insert.set_x_scale(scale_factor.x);
insert.set_y_scale(scale_factor.y);
insert.set_z_scale(scale_factor.z);
}
(
ObjectContextKind::Text {
horizontal_mode,
rotation,
insertion,
alignment,
},
EntityType::Text(text),
) => {
text.horizontal_alignment = text_horizontal(*horizontal_mode);
text.rotation = *rotation;
text.insertion_point.x = insertion.x;
text.insertion_point.y = insertion.y;
let point = text.alignment_point.get_or_insert(Vector3::ZERO);
point.x = alignment.x;
point.y = alignment.y;
}
(ObjectContextKind::MText(value), EntityType::MText(mtext)) => {
apply_mtext_context(mtext, value);
}
(ObjectContextKind::Dim(value), EntityType::Dimension(dimension)) => {
apply_dimension_context(dimension, value, doc);
}
(ObjectContextKind::MLeader(value), EntityType::MultiLeader(mleader)) => {
mleader.context.clone_from(value);
}
(
ObjectContextKind::MTextAttribute(value),
EntityType::AttributeEntity(attribute),
) => {
apply_attribute_context(
&mut attribute.insertion_point,
&mut attribute.alignment_point,
&mut attribute.rotation,
&mut attribute.horizontal_alignment,
&mut attribute.embedded_mtext,
value,
);
}
(
ObjectContextKind::MTextAttribute(value),
EntityType::AttributeDefinition(attribute),
) => {
apply_attribute_context(
&mut attribute.insertion_point,
&mut attribute.alignment_point,
&mut attribute.rotation,
&mut attribute.horizontal_alignment,
&mut attribute.embedded_mtext,
value,
);
}
(ObjectContextKind::Leader(value), EntityType::Leader(leader)) => {
leader.vertices = value
.points
.iter()
.map(|point| *point + value.insertion_offset)
.collect();
leader.horizontal_direction = value.x_direction;
leader.annotation_offset = value.endpoint_projection;
}
(
ObjectContextKind::Fcf {
location,
horizontal_direction,
},
EntityType::Tolerance(tolerance),
) => {
tolerance.insertion_point = *location;
tolerance.direction = *horizontal_direction;
}
(ObjectContextKind::HatchScale(value), EntityType::Hatch(hatch)) => {
apply_hatch_context(hatch, value);
}
(ObjectContextKind::HatchView(value), EntityType::Hatch(hatch)) => {
apply_hatch_context(hatch, &value.hatch);
hatch.normal = value.view_normal;
hatch.pattern_angle += value.view_rotation;
}
_ => {}
}
Cow::Owned(placed)
}
fn sync_mtext_context(context: &mut MTextContext, entity: &acadrust::entities::MText) {
context.attachment = entity.attachment_point as i32;
context.x_axis_dir = entity.dwg_x_direction.unwrap_or_else(|| {
Vector3::new(entity.rotation.cos(), entity.rotation.sin(), 0.0)
});
context.insertion = entity.insertion_point;
context.rect_width = entity.rectangle_width;
context.rect_height = entity.rectangle_height.unwrap_or(0.0);
context.extents_width = entity.extents_width;
context.extents_height = entity.extents_height;
context.column_type = entity.column_data.column_type as i32;
if context.column_type == 0 {
context.columns = None;
} else {
let columns = context
.columns
.get_or_insert_with(|| acadrust::objects::MTextColumns {
num_heights: 0,
width: 0.0,
gutter: 0.0,
auto_height: false,
flow_reversed: false,
heights: Vec::new(),
});
columns.num_heights = entity.column_data.column_count;
columns.width = entity.column_data.width;
columns.gutter = entity.column_data.gutter;
columns.auto_height = entity.column_data.auto_height;
columns.flow_reversed = entity.column_data.flow_reversed;
columns.heights.clone_from(&entity.column_data.heights);
}
}
fn sync_dimension_context(
context: &mut acadrust::objects::DimContext,
dimension: &acadrust::entities::Dimension,
block_handle: Option<Handle>,
) {
use acadrust::entities::Dimension;
use acadrust::objects::DimSubtype;
let base = dimension.base();
context.def_pt = Vector2::new(base.text_middle_point.x, base.text_middle_point.y);
context.is_def_textloc = base.text_user_positioned;
context.text_rotation = base.text_rotation;
context.flip_arrow1 = base.flip_arrow1;
context.flip_arrow2 = base.flip_arrow2;
if let Some(handle) = block_handle {
context.block = handle;
}
match (&mut context.subtype, dimension) {
(DimSubtype::Aligned { dimline_pt }, Dimension::Aligned(dim)) => {
*dimline_pt = dim.definition_point;
}
(DimSubtype::Aligned { dimline_pt }, Dimension::Linear(dim)) => {
*dimline_pt = dim.definition_point;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular2Ln(dim)) => {
*arc_pt = dim.dimension_arc;
}
(DimSubtype::Angular { arc_pt }, Dimension::Angular3Pt(dim)) => {
*arc_pt = dim.definition_point;
}
(
DimSubtype::Diametric {
first_arc_pt,
def_pt,
},
Dimension::Diameter(dim),
) => {
*first_arc_pt = dim.angle_vertex;
*def_pt = dim.definition_point;
}
(DimSubtype::Radial { first_arc_pt }, Dimension::Radius(dim)) => {
*first_arc_pt = dim.definition_point;
}
(
DimSubtype::RadialLarge {
ovr_center,
jog_point,
},
Dimension::LargeRadial(dim),
) => {
*ovr_center = dim.override_center;
*jog_point = dim.jog_point;
}
(
DimSubtype::Ordinate {
feature_location_pt,
leader_endpt,
},
Dimension::Ordinate(dim),
) => {
*feature_location_pt = dim.feature_location;
*leader_endpt = dim.leader_endpoint;
}
_ => {}
}
}
/// Copy an edited entity's placement back into its active per-scale leaf.
/// Geometry edits therefore remain visible at the current annotation scale and
/// round-trip as genuine `AcDb*ObjectContextData`, while the base entity stays
/// usable as the default representation.
pub fn sync_active_context_from_entity(
doc: &mut CadDocument,
entity_handle: Handle,
) -> bool {
let Some(leaf_handle) = active_object_context(doc, entity_handle).map(|leaf| leaf.handle)
else {
return false;
};
let Some(entity) = doc.get_entity(entity_handle).cloned() else {
return false;
};
let dim_block_handle = match &entity {
EntityType::Dimension(dimension) => doc
.block_records
.iter()
.find(|record| record.name.eq_ignore_ascii_case(&dimension.base().block_name))
.map(|record| record.handle),
_ => None,
};
let Some(ObjectType::ObjectContextData(leaf)) = doc.objects.get_mut(&leaf_handle) else {
return false;
};
match (&entity, &mut leaf.kind) {
(
EntityType::Insert(insert),
ObjectContextKind::BlkRef {
rotation,
insertion,
scale_factor,
},
) => {
*rotation = insert.rotation;
*insertion = insert.insert_point;
*scale_factor =
Vector3::new(insert.x_scale(), insert.y_scale(), insert.z_scale());
}
(
EntityType::Text(text),
ObjectContextKind::Text {
horizontal_mode,
rotation,
insertion,
alignment,
},
) => {
*horizontal_mode = match text.horizontal_alignment {
acadrust::entities::TextHorizontalAlignment::Left => 0,
acadrust::entities::TextHorizontalAlignment::Center => 1,
acadrust::entities::TextHorizontalAlignment::Right => 2,
acadrust::entities::TextHorizontalAlignment::Aligned => 3,
acadrust::entities::TextHorizontalAlignment::Middle => 4,
acadrust::entities::TextHorizontalAlignment::Fit => 5,
};
*rotation = text.rotation;
*insertion = Vector2::new(text.insertion_point.x, text.insertion_point.y);
let point = text.alignment_point.unwrap_or(Vector3::ZERO);
*alignment = Vector2::new(point.x, point.y);
}
(EntityType::MText(mtext), ObjectContextKind::MText(context)) => {
sync_mtext_context(context, mtext);
}
(EntityType::Dimension(dimension), ObjectContextKind::Dim(context)) => {
sync_dimension_context(context, dimension, dim_block_handle);
}
(EntityType::MultiLeader(mleader), ObjectContextKind::MLeader(context)) => {
context.clone_from(&mleader.context);
}
(
EntityType::AttributeEntity(attribute),
ObjectContextKind::MTextAttribute(context),
) => {
context.horizontal_mode = attribute.horizontal_alignment.to_value();
context.rotation = attribute.rotation;
context.insertion =
Vector2::new(attribute.insertion_point.x, attribute.insertion_point.y);
context.alignment =
Vector2::new(attribute.alignment_point.x, attribute.alignment_point.y);
if let (Some(embedded), Some(mtext)) =
(&mut context.context, &attribute.embedded_mtext)
{
sync_mtext_context(&mut embedded.mtext, mtext);
}
}
(
EntityType::AttributeDefinition(attribute),
ObjectContextKind::MTextAttribute(context),
) => {
context.horizontal_mode = attribute.horizontal_alignment.to_value();
context.rotation = attribute.rotation;
context.insertion =
Vector2::new(attribute.insertion_point.x, attribute.insertion_point.y);
context.alignment =
Vector2::new(attribute.alignment_point.x, attribute.alignment_point.y);
if let (Some(embedded), Some(mtext)) =
(&mut context.context, &attribute.embedded_mtext)
{
sync_mtext_context(&mut embedded.mtext, mtext);
}
}
(EntityType::Leader(leader), ObjectContextKind::Leader(context)) => {
context.points = leader
.vertices
.iter()
.map(|point| *point - context.insertion_offset)
.collect();
context.x_direction = leader.horizontal_direction;
context.endpoint_projection = leader.annotation_offset;
}
(
EntityType::Tolerance(tolerance),
ObjectContextKind::Fcf {
location,
horizontal_direction,
},
) => {
*location = tolerance.insertion_point;
*horizontal_direction = tolerance.direction;
}
(EntityType::Hatch(hatch), ObjectContextKind::HatchScale(context)) => {
context.pattern_lines.clone_from(&hatch.pattern.lines);
for line in &mut context.pattern_lines {
line.base_point.x -= context.pattern_base.x;
line.base_point.y -= context.pattern_base.y;
}
context.pattern_scale = hatch.pattern_scale;
context.loop_types = hatch
.paths
.iter()
.map(|path| path.flags.bits() as i32)
.collect();
}
(EntityType::Hatch(hatch), ObjectContextKind::HatchView(context)) => {
context.hatch.pattern_lines.clone_from(&hatch.pattern.lines);
for line in &mut context.hatch.pattern_lines {
line.base_point.x -= context.hatch.pattern_base.x;
line.base_point.y -= context.hatch.pattern_base.y;
}
context.hatch.pattern_scale = hatch.pattern_scale;
context.hatch.loop_types = hatch
.paths
.iter()
.map(|path| path.flags.bits() as i32)
.collect();
context.view_normal = hatch.normal;
}
_ => {}
}
true
}
/// Get the child dictionary stored under `key` in `parent_h`, creating an empty
/// one (owned by `parent_h`) and registering the entry when absent.
fn get_or_create_child_dict(doc: &mut CadDocument, parent_h: Handle, key: &str) -> Handle {
@ -489,4 +1050,3 @@ pub fn is_annotative(doc: &CadDocument, entity: &EntityType) -> bool {
_ => false,
}
}

View file

@ -358,9 +358,12 @@ fn build_defn(
let cap = br.entity_handles.len();
let mut subs: Vec<LocalSub> = Vec::with_capacity(cap);
for &eh in &br.entity_handles {
let Some(entity) = doc.get_entity(eh) else {
let Some(source_entity) = doc.get_entity(eh) else {
continue;
};
let contextual =
crate::scene::annotative::entity_for_active_context(doc, source_entity);
let entity = contextual.as_ref();
// Skip entities flagged invisible. Dynamic blocks (e.g. a visibility-
// state parametric block) keep the geometry for every state in one
// anonymous block and mark all but the active state's entities
@ -468,7 +471,9 @@ fn build_defn(
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 {
@ -505,6 +510,25 @@ fn build_defn(
}
}
}
if !used_baked {
for lw in
tessellate_sub_local(doc, entity, anno_scale, bg_color, depth_map)
{
subs.push(LocalSub::Wire(lw));
}
for insert in crate::entities::table::block_cell_inserts(
tab,
doc,
anno_scale,
) {
subs.push(LocalSub::Nested(build_nested_ref(
&insert,
doc,
bg_color,
depth_map,
)));
}
}
}
_ => {
// A wide polyline inside a block carries its `world_width` on
@ -539,6 +563,9 @@ fn build_nested_ref(
// 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
@ -553,10 +580,10 @@ fn build_nested_ref(
ins_pat_len,
ins_pat,
ins_lw_px,
color_is_byblock: nested_ins.common.color == AcadColor::ByBlock,
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: nested_ins.common.color == AcadColor::ByLayer,
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")
@ -601,7 +628,10 @@ fn tessellate_sub_local(
let (sub_color, pat_len, pat, lw_px, aci) = crate::scene::view::render::render_style_for(doc, sub);
let _ = bg_color;
let color_is_byblock = sub.common().color == AcadColor::ByBlock;
let has_book_color =
crate::scene::view::render::has_resolved_book_color(doc, sub);
let color_is_byblock =
!has_book_color && sub.common().color == AcadColor::ByBlock;
let lt_is_byblock = sub.common().linetype.eq_ignore_ascii_case("byblock");
let lw_is_byblock = matches!(sub.common().line_weight, LineWeight::ByBlock);
@ -609,7 +639,8 @@ fn tessellate_sub_local(
// INSERT's layer at expand time. Flag each ByLayer property so emit_wire
// can override the cached (layer-0-resolved) value with the insert layer's.
let on_l0 = crate::scene::view::render::is_effective_layer_zero(&sub.common().layer);
let color_l0 = on_l0 && sub.common().color == AcadColor::ByLayer;
let color_l0 =
!has_book_color && on_l0 && sub.common().color == AcadColor::ByLayer;
let lt_l0 = on_l0 && {
let lt = &sub.common().linetype;
lt.is_empty() || lt.eq_ignore_ascii_case("bylayer")

View file

@ -124,13 +124,41 @@ pub fn visualization_section(entity: &EntityType) -> Option<PropSection> {
1 => "ByBlock",
_ => "Custom",
};
let mut props = vec![Property {
label: "Material".into(),
field: "material",
value: PropValue::ReadOnly(material.into()),
}];
let handle_text = |handle: Option<acadrust::Handle>| {
handle
.filter(|handle| handle.is_valid())
.map_or_else(|| "None".to_string(), |handle| format!("{:X}", handle.value()))
};
props.extend([
Property {
label: "Visual style".into(),
field: "visual_style",
value: PropValue::ReadOnly(handle_text(common.full_visual_style_handle)),
},
Property {
label: "Face style".into(),
field: "face_visual_style",
value: PropValue::ReadOnly(handle_text(common.face_visual_style_handle)),
},
Property {
label: "Edge style".into(),
field: "edge_visual_style",
value: PropValue::ReadOnly(handle_text(common.edge_visual_style_handle)),
},
Property {
label: "Shadow flags".into(),
field: "shadow_flags",
value: PropValue::ReadOnly(common.shadow_flags.to_string()),
},
]);
Some(PropSection {
title: "3D Visualization".into(),
props: vec![Property {
label: "Material".into(),
field: "material",
value: PropValue::ReadOnly(material.into()),
}],
props,
})
}
@ -144,4 +172,3 @@ pub fn fallback_properties(_handle: Handle, entity: &EntityType) -> PropSection
}],
}
}

View file

@ -34,6 +34,7 @@ use crate::scene::convert::solid3d_tess::{
TRUCK_CHORD_FRAC,
};
use crate::scene::model::mesh_model::MeshLodSet;
use rustc_hash::FxHashMap;
/// Slightly over 2π so revolution builders close the loop.
const FULL: f64 = std::f64::consts::TAU + 0.2;
@ -91,9 +92,53 @@ pub fn tessellate_sat_truck(
let mut verts: Vec<[f64; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let face_materials: FxHashMap<i32, acadrust::Handle> = sat
.records
.iter()
.filter(|record| record.entity_type == "material-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let handle = record.token(3)?.as_integer()?;
(owner >= 0 && handle > 0)
.then(|| (owner, acadrust::Handle::new(handle as u64)))
})
.collect();
let face_colors: FxHashMap<i32, [f32; 4]> = sat
.records
.iter()
.filter(|record| record.entity_type == "color-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let value = record.token(3)?.as_integer()?;
let face_color = if (1..=255).contains(&value) {
acadrust::Color::from_index(value as i16)
} else if value > 257 {
acadrust::Color::from_true_color_value(value as i32)
} else {
return None;
};
Some((
owner,
{
let mut rgba =
crate::scene::convert::tess_util::aci_to_rgba(&face_color);
rgba[3] = color[3];
rgba
},
))
})
.collect();
let mut triangle_material_handles: Vec<Option<acadrust::Handle>> = Vec::new();
let mut triangle_colors: Vec<Option<[f32; 4]>> = Vec::new();
let face_record_indices: Vec<i32> = sat
.records
.iter()
.filter(|record| record.is_a("face"))
.map(|record| record.index)
.collect();
let mut complete = true;
for face in sat.faces().into_iter() {
for (face, face_record_index) in sat.faces().into_iter().zip(face_record_indices) {
let Some(surf_rec) = sat.resolve(face.surface()) else {
complete = false;
continue;
@ -121,6 +166,15 @@ pub fn tessellate_sat_truck(
}
if !appended {
complete = false;
} else {
let material = face_materials.get(&face_record_index).copied();
triangle_material_handles.extend(
std::iter::repeat(material).take((indices.len() - before) / 3),
);
let face_color = face_colors.get(&face_record_index).copied();
triangle_colors.extend(
std::iter::repeat(face_color).take((indices.len() - before) / 3),
);
}
}
@ -128,7 +182,16 @@ pub fn tessellate_sat_truck(
return None;
}
let mesh = finalize_mesh(name, verts, normals, indices, color, body_transform(sat));
let mesh = finalize_mesh(
name,
verts,
normals,
indices,
triangle_material_handles,
triangle_colors,
color,
body_transform(sat),
);
let mut set = MeshLodSet::from_lods(vec![mesh]);
set.complete = complete;
Some(set)
@ -166,7 +229,7 @@ fn bespoke_face(
tess_torus_face(sat, face, &t, LodConfig::HIGH, v, n, i);
}
}
"spline-surface" => {
"spline-surface" | "meshsurf-surface" | "bs3-surface" => {
crate::scene::convert::spline_tess::tess_spline_face(
sat,
face,

View file

@ -12,12 +12,13 @@
// editing never mistakes them for closed topology.
use rustc_hash::FxHashSet as HashSet;
use rustc_hash::FxHashMap;
use std::f64::consts::TAU;
use acadrust::entities::acis::types::Sense;
use acadrust::entities::acis::{
SabReader, SatCoedge, SatConeSurface, SatDocument, SatEdge, SatEllipseCurve, SatFace,
SatIntCurve, SatLoop, SatPlaneSurface, SatPoint, SatPointer, SatSphereSurface, SatToken,
SatIntCurve, SatLoop, SatPlaneSurface, SatPoint, SatPointer, SatSphereSurface,
SatTorusSurface, SatVertex,
};
use acadrust::entities::{Body, Region, Solid3D};
@ -127,9 +128,48 @@ fn tessellate_sat(
let mut verts: Vec<[f64; 3]> = Vec::new();
let mut normals: Vec<[f32; 3]> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let face_materials: FxHashMap<i32, acadrust::Handle> = sat
.records
.iter()
.filter(|record| record.entity_type == "material-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let handle = record.token(3)?.as_integer()?;
(owner >= 0 && handle > 0)
.then(|| (owner, acadrust::Handle::new(handle as u64)))
})
.collect();
let face_colors: FxHashMap<i32, [f32; 4]> = sat
.records
.iter()
.filter(|record| record.entity_type == "color-adesk-attrib")
.filter_map(|record| {
let owner = record.token_pointer(2)?.0;
let value = record.token(3)?.as_integer()?;
let color_value = if (1..=255).contains(&value) {
Some(acadrust::Color::from_index(value as i16))
} else if value > 257 {
Some(acadrust::Color::from_true_color_value(value as i32))
} else {
None
}?;
let mut rgba =
crate::scene::convert::tess_util::aci_to_rgba(&color_value);
rgba[3] = color[3];
Some((owner, rgba))
})
.collect();
let mut triangle_material_handles: Vec<Option<acadrust::Handle>> = Vec::new();
let mut triangle_colors: Vec<Option<[f32; 4]>> = Vec::new();
let face_record_indices: Vec<i32> = sat
.records
.iter()
.filter(|record| record.is_a("face"))
.map(|record| record.index)
.collect();
let mut complete = true;
for face in sat.faces() {
for (face, face_record_index) in sat.faces().into_iter().zip(face_record_indices) {
let surf_ptr = face.surface();
let Some(surf_rec) = sat.resolve(surf_ptr) else {
complete = false;
@ -189,7 +229,7 @@ fn tessellate_sat(
);
}
}
"spline-surface" => {
"spline-surface" | "meshsurf-surface" | "bs3-surface" => {
crate::scene::convert::spline_tess::tess_spline_face(
sat,
&face,
@ -203,13 +243,31 @@ fn tessellate_sat(
}
if indices.len() == before {
complete = false;
} else {
let material = face_materials.get(&face_record_index).copied();
triangle_material_handles.extend(
std::iter::repeat(material).take((indices.len() - before) / 3),
);
let face_color = face_colors.get(&face_record_index).copied();
triangle_colors.extend(
std::iter::repeat(face_color).take((indices.len() - before) / 3),
);
}
}
if indices.is_empty() {
return None;
}
Some((
finalize_mesh(name, verts, normals, indices, color, xform),
finalize_mesh(
name,
verts,
normals,
indices,
triangle_material_handles,
triangle_colors,
color,
xform,
),
complete,
))
}
@ -937,34 +995,43 @@ fn tessellate_sat_lods(
/// record (`<3×3> <tx ty tz> <scale> rotate reflect shear`). `None` when the
/// document has no transform (treated as identity).
pub(crate) fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f64)> {
let t = sat.records.iter().find(|r| r.entity_type == "transform")?;
// The transform record's numeric payload is its first 13 numeric values:
// 3×3 matrix, translation, scale. A leading book-keeping pointer (`$-1`)
// and the trailing rotate/reflect/shear flags aren't numeric, so
// collecting numeric tokens skips them — reading by raw token index would
// be thrown off by the leading pointer. SAT text tokenizes the payload as
// 13 individual floats, but the SAB reader groups the matrix rows and the
// translation into `Position` triplets, so those must be flattened too.
let mut v: Vec<f64> = Vec::with_capacity(13);
for tok in &t.tokens {
if v.len() >= 13 {
let transform = sat
.records
.iter()
.find(|record| record.entity_type == "transform")?;
let mut values = Vec::with_capacity(13);
for token in &transform.tokens {
if values.len() >= 13 {
break;
}
match tok {
SatToken::Position(x, y, z) => v.extend([*x, *y, *z]),
_ => {
if let Some(f) = tok.as_float() {
v.push(f);
if let Some((components, len)) = token.coordinate_components() {
values.extend_from_slice(&components[..len]);
} else if let Some(value) = token.as_float() {
values.push(value);
} else if let Some(text) = token.as_string() {
// Some ASM SAB bodies pack the complete SAT transform payload in
// one long-string token instead of individual numeric tokens.
for word in text.split_ascii_whitespace() {
let Ok(value) = word.parse::<f64>() else {
break;
};
values.push(value);
if values.len() >= 13 {
break;
}
}
}
}
if v.len() < 13 {
return None;
}
let m = [v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8]];
let tr = [v[9], v[10], v[11]];
Some((m, tr, v[12]))
(values.len() >= 13).then(|| {
(
[
values[0], values[1], values[2], values[3], values[4], values[5], values[6],
values[7], values[8],
],
[values[9], values[10], values[11]],
values[12],
)
})
}
/// Apply a body placement transform to a mesh. ACIS treats points as row
@ -985,6 +1052,8 @@ pub(crate) fn finalize_mesh(
verts: Vec<[f64; 3]>,
normals: Vec<[f32; 3]>,
indices: Vec<u32>,
triangle_material_handles: Vec<Option<acadrust::Handle>>,
triangle_colors: Vec<Option<[f32; 4]>>,
color: [f32; 4],
xform: Option<([f64; 9], [f64; 3], f64)>,
) -> MeshModel {
@ -1031,6 +1100,8 @@ pub(crate) fn finalize_mesh(
verts_low: lo,
normals,
indices,
triangle_material_handles,
triangle_colors,
color,
selected: false,
}
@ -1088,6 +1159,76 @@ fn parse_acis(
None
}
fn remap_acis_material_bindings(
set: &mut MeshLodSet,
acis: &acadrust::entities::AcisData,
) {
if acis.materials.is_empty() {
return;
}
for lod in &mut set.lods {
for handle in lod.triangle_material_handles.iter_mut().flatten() {
let reference = handle.value() as i32;
if let Some(material_handle) = acis.materials.iter().find_map(|binding| {
(binding.absolute_reference == reference || binding.array_index == reference)
.then_some(binding.material_handle)
.flatten()
}) {
*handle = material_handle;
}
}
}
}
fn attach_stored_silhouettes(
set: &mut MeshLodSet,
silhouettes: &[acadrust::entities::Silhouette],
) {
use crate::scene::model::mesh_model::StoredSilhouette;
set.stored_silhouettes = silhouettes
.iter()
.filter_map(|silhouette| {
let mut edge_verts = Vec::new();
let mut edge_verts_low = Vec::new();
for wire in &silhouette.wires {
for segment in wire.points.windows(2) {
for point in segment {
let point = crate::entities::solid3d::wire_point(wire, point);
let high = [point.x as f32, point.y as f32, point.z as f32];
edge_verts.push(high);
edge_verts_low.push([
(point.x - high[0] as f64) as f32,
(point.y - high[1] as f64) as f32,
(point.z - high[2] as f64) as f32,
]);
}
}
}
(!edge_verts.is_empty()).then_some(StoredSilhouette {
viewport_id: silhouette.viewport_id,
view_direction: [
silhouette.view_direction.x as f32,
silhouette.view_direction.y as f32,
silhouette.view_direction.z as f32,
],
up_vector: [
silhouette.up_vector.x as f32,
silhouette.up_vector.y as f32,
silhouette.up_vector.z as f32,
],
target: [
silhouette.target.x as f32,
silhouette.target.y as f32,
silhouette.target.z as f32,
],
is_perspective: silhouette.is_perspective,
edge_verts,
edge_verts_low,
})
})
.collect();
}
/// Tessellate a `Region` entity (2D planar ACIS body) at all three LOD levels.
pub fn tessellate_region(
region: &Region,
@ -1101,7 +1242,10 @@ pub fn tessellate_region(
&region.acis_data.sab_data,
)?;
let name = region.common.handle.value().to_string();
tessellate_acis(&sat, name, color, facet_res, isolines)
let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?;
remap_acis_material_bindings(&mut set, &region.acis_data);
attach_stored_silhouettes(&mut set, &region.silhouettes);
Some(set)
}
/// Tessellate a `Body` entity (3D ACIS body) at all three LOD levels.
@ -1117,7 +1261,10 @@ pub fn tessellate_body(
&body.acis_data.sab_data,
)?;
let name = body.common.handle.value().to_string();
tessellate_acis(&sat, name, color, facet_res, isolines)
let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?;
remap_acis_material_bindings(&mut set, &body.acis_data);
attach_stored_silhouettes(&mut set, &body.silhouettes);
Some(set)
}
/// Tessellate a `Surface` entity (ACAD_SURFACE family) at all three LOD
@ -1136,7 +1283,12 @@ pub fn tessellate_surface(
&surface.acis_data.sab_data,
)?;
let name = surface.common.handle.value().to_string();
tessellate_acis(&sat, name, color, facet_res, isolines)
let surface_isolines = usize::from(surface.u_isolines.max(surface.v_isolines).max(0) as u16);
let mut set =
tessellate_acis(&sat, name, color, facet_res, surface_isolines.max(isolines))?;
remap_acis_material_bindings(&mut set, &surface.acis_data);
attach_stored_silhouettes(&mut set, &surface.silhouettes);
Some(set)
}
/// Tessellate a `Solid3D` entity at all three LOD levels.
@ -1156,7 +1308,10 @@ pub fn tessellate_solid3d(
&solid.acis_data.sab_data,
)?;
let name = solid.common.handle.value().to_string();
tessellate_acis(&sat, name, color, facet_res, isolines)
let mut set = tessellate_acis(&sat, name, color, facet_res, isolines)?;
remap_acis_material_bindings(&mut set, &solid.acis_data);
attach_stored_silhouettes(&mut set, &solid.silhouettes);
Some(set)
}
// ── Topology helpers ──────────────────────────────────────────────────────────

View file

@ -357,6 +357,8 @@ pub(crate) fn tessellate_entity(
// by the viewport's GPU uniform so it never changes resident wire content.
paper_space: bool,
) -> Vec<WireModel> {
let contextual = crate::scene::annotative::entity_for_active_context(document, e);
let e = contextual.as_ref();
let h = e.common().handle;
let sel = selected.contains(&h);
@ -518,9 +520,22 @@ pub(crate) fn tessellate_entity(
// blob, the vector preview its author cached for exactly this case. AutoCAD
// draws that when the object enabler is missing; draw it too, so the entity
// occupies its real place instead of silently disappearing.
if let EntityType::Unknown(_) = e {
if let Some(blob) = e.common().graphic_data.as_ref() {
let dec = convert::proxy_graphics::decode(blob);
let proxy_blob: Option<std::borrow::Cow<'_, [u8]>> = match e {
EntityType::Unknown(_) => e
.common()
.graphic_data
.as_deref()
.map(std::borrow::Cow::Borrowed),
EntityType::Extended(entity) => match &entity.data {
acadrust::entities::ExtendedEntityData::Proxy(proxy) => {
Some(std::borrow::Cow::Owned(proxy.graphics.data()))
}
_ => None,
},
_ => None,
};
if let Some(blob) = proxy_blob {
let dec = convert::proxy_graphics::decode(blob.as_ref());
if !dec.polylines.is_empty() || !dec.texts.is_empty() {
use crate::scene::convert::proxy_graphics::ProxyColor;
use std::collections::BTreeMap;
@ -618,7 +633,6 @@ pub(crate) fn tessellate_entity(
return wires;
}
}
}
}
// ── Section symbol (AcDbSectionSymbol): draw the "A-A" cut mark ──────────
@ -744,10 +758,13 @@ pub(crate) fn tessellate_entity(
// Sub-entities inside *D### / DIMBLOCK## blocks
// typically use ByBlock color/linetype/lineweight —
// they should inherit from the Dimension entity.
let sub_color_is_byblock =
sub.common().color == acadrust::types::Color::ByBlock;
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 =
view::render::is_effective_layer_zero(&sub.common().layer)
!has_book_color
&& view::render::is_effective_layer_zero(&sub.common().layer)
&& sub.common().color == acadrust::types::Color::ByLayer;
let sub_wires = tessellate_entity(
document,
@ -898,10 +915,13 @@ pub(crate) fn tessellate_entity(
let Some(sub) = document.get_entity(eh) else {
continue;
};
let sub_color_is_byblock =
sub.common().color == acadrust::types::Color::ByBlock;
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 =
view::render::is_effective_layer_zero(&sub.common().layer)
!has_book_color
&& view::render::is_effective_layer_zero(&sub.common().layer)
&& sub.common().color == acadrust::types::Color::ByLayer;
let mut placed = sub.clone();
{
@ -976,6 +996,22 @@ pub(crate) fn tessellate_entity(
line_weight_px,
table_anno,
);
for insert in
crate::entities::table::block_cell_inserts(tab, document, table_anno)
{
wires.extend(tessellate_entity(
document,
selected,
active_viewport,
bg_color,
1.0,
&EntityType::Insert(insert),
block_cache,
view_aabb,
world_per_pixel,
paper_space,
));
}
if !wires.is_empty() {
let aabb = entity_aabb(e);
for w in &mut wires {

View file

@ -1723,7 +1723,8 @@ fn solid_wire_fallback(entity: &EntityType) -> Vec<[f64; 3]> {
continue;
}
for v in &wire.points {
pts.push([v.x, v.y, v.z]);
let transformed = crate::entities::solid3d::wire_point(wire, v);
pts.push([transformed.x, transformed.y, transformed.z]);
}
// NaN sentinel separates distinct wire segments.
pts.push([f64::NAN, f64::NAN, f64::NAN]);

View file

@ -140,7 +140,13 @@ impl Scene {
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
) {
let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines)
@ -206,7 +212,28 @@ impl Scene {
if let Some(model) = image_seed {
self.images.insert(handle, model);
}
if let Some(model) = mesh_seed {
if let Some(mut model) = mesh_seed {
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
let material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut model,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut model,
&self.document,
entity,
);
}
self.meshes.insert(handle, model);
}
// Delta-undo: the new handle's before-image is "nothing" (it did not
@ -330,7 +357,13 @@ impl Scene {
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
) {
let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines)
@ -369,7 +402,28 @@ impl Scene {
if let Some(model) = image_seed {
self.images.insert(handle, model);
}
if let Some(model) = mesh_seed {
if let Some(mut model) = mesh_seed {
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
let material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut model,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut model,
&self.document,
entity,
);
}
self.meshes.insert(handle, model);
}
@ -448,6 +502,9 @@ impl Scene {
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
) {
return None;
}
@ -473,7 +530,24 @@ impl Scene {
facet_res,
isolines,
)
.map(|mesh| {
.map(|mut mesh| {
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity.as_ref(),
color,
None,
self.material_base_dir.as_deref(),
)
.apply_to_with_face_overrides(
&mut mesh,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut mesh,
&self.document,
entity.as_ref(),
);
let mesh = if top_level {
offset_mesh_lod_set(mesh)
} else {
@ -758,8 +832,29 @@ impl Scene {
))
})
.flat_map(|(&handle, model)| {
let entity = self.document.get_entity(handle);
let mut m = model.clone();
let contextual = self
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_active_context(
&self.document,
entity,
)
});
let entity = contextual.as_deref();
let mut m = match entity {
Some(EntityType::Hatch(dxf))
if crate::scene::annotative::active_object_context(
&self.document,
handle,
)
.is_some() =>
{
Self::hatch_model_from_dxf(dxf, model.color)
.unwrap_or_else(|| model.clone())
}
_ => model.clone(),
};
// Optional solid backdrop drawn behind the pattern/gradient when
// the hatch carries a HATCHBACKGROUNDCOLOR. Same draw_depth +
// emitted first so LessEqual layering keeps it underneath.
@ -958,7 +1053,9 @@ impl Scene {
out
}
for entity in self.document.entities() {
let EntityType::Insert(ins) = entity else {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, entity);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
if ins.common.invisible || layer_hidden(&ins.common.layer) {
@ -1034,6 +1131,10 @@ impl Scene {
)> = explode_including_dims(ins, &self.document)
.into_iter()
.filter(|e| !offscale(e))
.map(|e| {
crate::scene::annotative::entity_for_active_context(&self.document, &e)
.into_owned()
})
.map(|e| (normalize(e), 0usize, ins_color, l0, (base_depth, half_gap)))
.collect();
while let Some((sub, depth, sub_ins_color, sub_l0, (d_base, d_half))) = stack.pop() {
@ -1050,14 +1151,25 @@ impl Scene {
let on_l0 = crate::scene::view::render::is_effective_layer_zero(
&nins.common.layer,
);
let child_ins_color = if nins.common.color == Color::ByBlock {
let nested_entity = EntityType::Insert(nins.clone());
let has_book_color =
crate::scene::view::render::has_resolved_book_color(
&self.document,
&nested_entity,
);
let child_ins_color = if !has_book_color
&& nins.common.color == Color::ByBlock
{
sub_ins_color
} else if on_l0 && nins.common.color == Color::ByLayer {
} else if !has_book_color
&& on_l0
&& nins.common.color == Color::ByLayer
{
sub_l0.color
} else {
crate::scene::view::render::render_style_for(
&self.document,
&EntityType::Insert(nins.clone()),
&nested_entity,
)
.0
};
@ -1081,6 +1193,12 @@ impl Scene {
if offscale(&e) {
continue;
}
let e =
crate::scene::annotative::entity_for_active_context(
&self.document,
&e,
)
.into_owned();
stack.push((
normalize(e),
depth + 1,
@ -1235,7 +1353,9 @@ impl Scene {
// apply_rotation) rather than through Insert::explode — the latter
// double-scales the u/v basis.
for entity in self.document.entities() {
let EntityType::Insert(ins) = entity else {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, entity);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
let c = &ins.common;
@ -1927,7 +2047,13 @@ impl Scene {
/// at load, so a pattern-scale / background / boundary edit stays
/// invisible until the cached model is refreshed (#415).
pub fn refresh_fill_model(&mut self, handle: Handle) {
let new_model = match self.document.get_entity(handle) {
let contextual = self
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_active_context(&self.document, entity)
});
let new_model = match contextual.as_deref() {
Some(EntityType::Hatch(dxf)) => {
let color = convert::tess_util::aci_to_rgba(&dxf.common.color);
Self::hatch_model_from_dxf(dxf, color)
@ -1955,7 +2081,11 @@ impl Scene {
.document
.entities()
.filter_map(|e| match e {
EntityType::Hatch(h) => Some((h.common.handle, e.clone())),
EntityType::Hatch(h) => Some((
h.common.handle,
crate::scene::annotative::entity_for_active_context(&self.document, e)
.into_owned(),
)),
EntityType::Solid(s) => Some((s.common.handle, e.clone())),
_ => None,
})
@ -2047,7 +2177,13 @@ impl Scene {
.document
.entities()
.filter_map(|e| match e {
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) => {
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_) => {
let handle = e.common().handle;
// Incremental (post-xref) pass: leave already-tessellated
// host solids untouched, only build the newly merged ones.
@ -2073,9 +2209,26 @@ impl Scene {
let built: Vec<(Handle, MeshLodSet, bool)> = entries
.into_par_iter()
.filter_map(|(handle, entity, color, top_level)| {
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|m| {
let m = if top_level { offset_mesh_lod_set(m) } else { m };
(handle, m, top_level)
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|mut mesh| {
let material = crate::scene::model::material_model::resolve_material_with_base(
&self.document,
&entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut mesh,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut mesh,
&self.document,
&entity,
);
let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh };
(handle, mesh, top_level)
})
})
.collect();

View file

@ -417,7 +417,7 @@ pub struct OpenTimings {
/// Intended to run on a background thread during file load.
#[cfg(target_arch = "wasm32")]
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
build_derived_caches_impl(doc, None)
build_derived_caches_impl(doc, None, None)
}
/// Build open-time caches while reporting monotonic progress in 0..=10000.
@ -427,13 +427,15 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
pub fn build_derived_caches_with_progress(
doc: &CadDocument,
progress: &(dyn Fn(u16) + Sync),
material_base_dir: Option<&std::path::Path>,
) -> DerivedCaches {
build_derived_caches_impl(doc, Some(progress))
build_derived_caches_impl(doc, Some(progress), material_base_dir)
}
fn build_derived_caches_impl(
doc: &CadDocument,
progress: Option<&(dyn Fn(u16) + Sync)>,
material_base_dir: Option<&std::path::Path>,
) -> DerivedCaches {
// A new drawing must not inherit the previous one's resolved images — drop
// the memoised set so each reference re-reads / re-fetches once here (and
@ -500,7 +502,10 @@ fn build_derived_caches_impl(
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_) => mesh_handles.push(h),
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_) => mesh_handles.push(h),
_ => {}
}
if let Some(c) = offset_centroid(e, model_block, &prep) {
@ -540,7 +545,9 @@ fn build_derived_caches_impl(
let hatches: HashMap<Handle, HatchModel> = hatch_handles
.par_iter()
.filter_map(|&handle| {
let e = doc.get_entity(handle)?;
let source = doc.get_entity(handle)?;
let contextual = annotative::entity_for_active_context(doc, source);
let e = contextual.as_ref();
let (raw, ..) = view::render::render_style_for(doc, e);
let color = view::render::adapt_to_bg(raw, LOAD_BG);
let model = match e {
@ -607,11 +614,28 @@ fn build_derived_caches_impl(
let e = doc.get_entity(handle)?;
let (raw, ..) = view::render::render_style_for(doc, e);
let color = view::render::adapt_to_bg(raw, LOAD_BG);
let material = crate::scene::model::material_model::resolve_material_with_base(
doc,
e,
color,
None,
material_base_dir,
);
let top_level = layout_blocks.contains(&e.common().owner_handle);
let result = crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines)
.map(|m| {
let m = if top_level { offset_mesh_lod_set(m) } else { m };
(handle, m, top_level)
.map(|mut mesh| {
material.apply_to_with_face_overrides(
&mut mesh,
doc,
material_base_dir,
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut mesh,
doc,
e,
);
let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh };
(handle, mesh, top_level)
});
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
if done & 0xff == 0 || done == detail_total {
@ -1007,12 +1031,104 @@ fn transform_block_mesh_lod_set(
) -> MeshLodSet {
use acadrust::types::Vector3;
let mut out = set.clone();
// Silhouette generators are world-space and untransformed here, so a block
// instance would silhouette against the block-local pose. Drop them: a
// block-internal solid keeps its baked isolines and feature edges, just not
// the per-frame silhouette (deferred until the generators track the INSTANCE
// transform).
out.curved_gens.clear();
let transform_direction = |direction: [f32; 3]| {
let transformed = xform.apply_rotation(Vector3::new(
direction[0] as f64,
direction[1] as f64,
direction[2] as f64,
));
let length = transformed.length();
if length > 1e-12 {
[
(transformed.x / length) as f32,
(transformed.y / length) as f32,
(transformed.z / length) as f32,
]
} else {
direction
}
};
let scale_x = xform.apply_rotation(Vector3::UNIT_X).length();
let scale_y = xform.apply_rotation(Vector3::UNIT_Y).length();
let scale_z = xform.apply_rotation(Vector3::UNIT_Z).length();
let uniform_scale = (scale_x + scale_y + scale_z) / 3.0;
let is_uniform = (scale_x - uniform_scale).abs() <= uniform_scale.abs().max(1.0) * 1e-8
&& (scale_y - uniform_scale).abs() <= uniform_scale.abs().max(1.0) * 1e-8
&& (scale_z - uniform_scale).abs() <= uniform_scale.abs().max(1.0) * 1e-8;
if is_uniform {
let transform_split = |high: &mut [f32; 3], low: &mut [f32; 3]| {
let transformed = xform.apply(Vector3::new(
high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
high[2] as f64 + low[2] as f64,
));
*high = [
transformed.x as f32,
transformed.y as f32,
transformed.z as f32,
];
*low = [
(transformed.x - high[0] as f64) as f32,
(transformed.y - high[1] as f64) as f32,
(transformed.z - high[2] as f64) as f32,
];
};
for generator in &mut out.curved_gens {
match generator {
crate::scene::model::mesh_model::CurvedGen::Cone {
base,
base_low,
axis,
u_dir,
v_dir,
radius,
h_max,
..
} => {
transform_split(base, base_low);
*axis = transform_direction(*axis);
*u_dir = transform_direction(*u_dir);
*v_dir = transform_direction(*v_dir);
*radius *= uniform_scale as f32;
*h_max *= uniform_scale as f32;
}
crate::scene::model::mesh_model::CurvedGen::Sphere {
center,
center_low,
pole,
u_dir,
v_dir,
radius,
..
} => {
transform_split(center, center_low);
*pole = transform_direction(*pole);
*u_dir = transform_direction(*u_dir);
*v_dir = transform_direction(*v_dir);
*radius *= uniform_scale as f32;
}
crate::scene::model::mesh_model::CurvedGen::Torus {
center,
center_low,
axis,
u_dir,
v_dir,
major,
minor,
..
} => {
transform_split(center, center_low);
*axis = transform_direction(*axis);
*u_dir = transform_direction(*u_dir);
*v_dir = transform_direction(*v_dir);
*major *= uniform_scale as f32;
*minor *= uniform_scale as f32;
}
}
}
} else {
out.curved_gens.clear();
}
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
@ -1086,6 +1202,41 @@ fn transform_block_mesh_lod_set(
];
}
}
for silhouette in &mut out.stored_silhouettes {
silhouette.view_direction = transform_direction(silhouette.view_direction);
silhouette.up_vector = transform_direction(silhouette.up_vector);
let target = xform.apply(Vector3::new(
silhouette.target[0] as f64,
silhouette.target[1] as f64,
silhouette.target[2] as f64,
));
silhouette.target = [target.x as f32, target.y as f32, target.z as f32];
let count = silhouette.edge_verts.len();
if silhouette.edge_verts_low.len() != count {
silhouette.edge_verts_low = vec![[0.0; 3]; count];
}
for (high, low) in silhouette
.edge_verts
.iter_mut()
.zip(silhouette.edge_verts_low.iter_mut())
{
let transformed = xform.apply(Vector3::new(
high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
high[2] as f64 + low[2] as f64,
));
*high = [
transformed.x as f32,
transformed.y as f32,
transformed.z as f32,
];
*low = [
(transformed.x - high[0] as f64) as f32,
(transformed.y - high[1] as f64) as f32,
(transformed.z - high[2] as f64) as f32,
];
}
}
if min_x.is_finite() {
out.world_aabb = [min_x, min_y, max_x, max_y];
}
@ -1120,6 +1271,14 @@ pub(in crate::scene) struct NavPerfSample {
pub(in crate::scene) build_ms: f64,
}
#[derive(Clone)]
struct BlockMeshInherit {
insert_color: [f32; 4],
layer0_color: [f32; 4],
insert_material: crate::scene::model::material_model::MeshMaterial,
layer0_material: crate::scene::model::material_model::MeshMaterial,
}
pub struct Scene {
pub camera: Rc<RefCell<Camera>>,
/// Model-space tiled viewport layout. One full-window tile by default;
@ -1362,6 +1521,9 @@ pub struct Scene {
/// Top-level (layout-owned) solids only, stored in the offset-relative
/// render frame and drawn flat.
pub meshes: HashMap<Handle, MeshLodSet>,
/// Directory of the opened drawing, used to resolve relative AcDbMaterial
/// bitmap maps. `None` for unsaved drawings and browser uploads.
pub material_base_dir: Option<std::path::PathBuf>,
/// Meshes of block-definition solids, kept in *block-local* coordinates
/// (no world_offset). They are not drawn directly; each INSERT of the
/// owning block emits a transformed instance so a block placed at an
@ -1595,6 +1757,7 @@ impl Scene {
viewcube_ucs: glam::Mat4::IDENTITY,
hatches: HashMap::default(),
meshes: HashMap::default(),
material_base_dir: None,
block_meshes: HashMap::default(),
solid_models: HashMap::default(),
images: HashMap::default(),
@ -2100,12 +2263,30 @@ impl Scene {
/// in the same offset-relative frame the mesh pipeline uses, so the mesh is
/// stored as-is (Model-tab geometry is authored at world_offset 0).
pub fn register_solid_model(&mut self, handle: Handle, solid: truck_modeling::Solid) {
let color = self
.document
.get_entity(handle)
let entity = self.document.get_entity(handle);
let color = entity
.map(|e| self.render_style(e).0)
.unwrap_or([0.8, 0.8, 0.85, 1.0]);
if let Some(set) = crate::scene::model::solid_model::mesh_from_solid(&solid, color) {
if let Some(mut set) = crate::scene::model::solid_model::mesh_from_solid(&solid, color) {
if let Some(entity) = entity {
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
)
.apply_to_with_face_overrides(
&mut set,
&self.document,
self.material_base_dir.as_deref(),
);
crate::scene::model::visual_style_model::apply_mesh_visual_style(
&mut set,
&self.document,
entity,
);
}
self.meshes.insert(handle, set);
}
self.solid_models.insert(handle, solid);
@ -2125,27 +2306,47 @@ impl Scene {
// (instanced per INSERT), so a solid recolours wherever it lives.
// During REFEDIT, solids outside the edited set render faded.
let bg = self.bg_color;
let colors: HashMap<Handle, [f32; 4]> = self
let materials: HashMap<Handle, crate::scene::model::material_model::MeshMaterial> = self
.meshes
.keys()
.chain(self.block_meshes.keys())
.filter_map(|&h| {
self.document.get_entity(h).map(|e| {
let mut c = self.render_style(e).0;
let color = self.render_style(e).0;
let mut material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
e,
color,
None,
self.material_base_dir.as_deref(),
);
if let Some(keep) = &self.refedit_keep {
if !keep.contains(&h) {
c = crate::scene::cache::block_cache::fade_toward_bg(c, bg);
material.diffuse = crate::scene::cache::block_cache::fade_toward_bg(
material.diffuse,
bg,
);
}
}
(h, c)
(h, material)
})
})
.collect();
for (h, set) in self.meshes.iter_mut().chain(self.block_meshes.iter_mut()) {
if let Some(&c) = colors.get(h) {
for lod in &mut set.lods {
lod.color = c;
}
if let Some(material) = materials.get(h) {
material.apply_to_with_face_overrides(
set,
&self.document,
self.material_base_dir.as_deref(),
);
}
if let Some(entity) = self.document.get_entity(*h) {
crate::scene::model::visual_style_model::apply_mesh_visual_style(
set,
&self.document,
entity,
);
}
}
}
@ -2153,33 +2354,62 @@ impl Scene {
/// Recolour only the named cached solids after a property edit.
pub fn recolor_meshes_for_handles(&mut self, handles: &[Handle]) {
let bg = self.bg_color;
let colors: HashMap<Handle, [f32; 4]> = handles
let materials: HashMap<Handle, crate::scene::model::material_model::MeshMaterial> = handles
.iter()
.filter_map(|&handle| {
self.document.get_entity(handle).map(|entity| {
let mut color = self.render_style(entity).0;
let color = self.render_style(entity).0;
let mut material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
if self
.refedit_keep
.as_ref()
.is_some_and(|keep| !keep.contains(&handle))
{
color = crate::scene::cache::block_cache::fade_toward_bg(color, bg);
material.diffuse = crate::scene::cache::block_cache::fade_toward_bg(
material.diffuse,
bg,
);
}
(handle, color)
(handle, material)
})
})
.collect();
for handle in handles {
let Some(color) = colors.get(handle) else {
let Some(material) = materials.get(handle) else {
continue;
};
if let Some(set) = self.meshes.get_mut(handle) {
for lod in &mut set.lods {
lod.color = *color;
material.apply_to_with_face_overrides(
set,
&self.document,
self.material_base_dir.as_deref(),
);
if let Some(entity) = self.document.get_entity(*handle) {
crate::scene::model::visual_style_model::apply_mesh_visual_style(
set,
&self.document,
entity,
);
}
} else if let Some(set) = self.block_meshes.get_mut(handle) {
for lod in &mut set.lods {
lod.color = *color;
material.apply_to_with_face_overrides(
set,
&self.document,
self.material_base_dir.as_deref(),
);
if let Some(entity) = self.document.get_entity(*handle) {
crate::scene::model::visual_style_model::apply_mesh_visual_style(
set,
&self.document,
entity,
);
}
}
}
@ -3996,6 +4226,9 @@ impl Scene {
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
) {
continue;
}
@ -4073,6 +4306,9 @@ impl Scene {
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
| EntityType::Mesh(_)
| EntityType::PolygonMesh(_)
| EntityType::PolyfaceMesh(_)
) {
continue;
}
@ -4677,7 +4913,10 @@ impl Scene {
return Vec::new();
}
let mut out = Vec::new();
for e in self.document.entities() {
for source in self.document.entities() {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, source);
let e = contextual.as_ref();
if e.common().owner_handle != layout_block {
continue;
}
@ -4704,12 +4943,31 @@ impl Scene {
.color,
bg,
);
let inherit = BlockMeshInherit {
insert_color: ins_color,
layer0_color: l0,
insert_material:
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
e,
ins_color,
None,
self.material_base_dir.as_deref(),
),
layer0_material:
crate::scene::model::material_model::resolve_layer_material_with_base(
&self.document,
&ins.common.layer,
l0,
self.material_base_dir.as_deref(),
),
};
let start = out.len();
self.expand_block_meshes(
&ins.block_name,
&ins.get_transform(),
0,
Some((ins_color, l0)),
Some(inherit),
&mut out,
);
// Tag the instanced meshes with the parent INSERT handle so the
@ -4734,10 +4992,9 @@ impl Scene {
accum: &acadrust::types::Transform,
depth: usize,
// Block-child colour inheritance sources, bg-adapted:
// `(insert_color, insert_layer_color)`. `Some` only on the render path;
// pick paths pass `None` (colour irrelevant). Drives the ByBlock /
// layer-0 overrides for block-internal solids (#221).
inherit: Option<([f32; 4], [f32; 4])>,
// `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>,
) {
if depth > 16 {
@ -4748,9 +5005,12 @@ impl Scene {
};
let handles: Vec<Handle> = br.entity_handles.clone();
for h in handles {
let Some(e) = self.document.get_entity(h) else {
let Some(source) = self.document.get_entity(h) else {
continue;
};
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, source);
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_entity_visible(h) {
@ -4758,7 +5018,9 @@ impl Scene {
}
if let EntityType::Insert(ins) = e {
let composed = ins.get_transform().then(accum);
let child = inherit.map(|(pc, pl0)| self.chain_mesh_inherit(ins, pc, pl0));
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);
} else if let Some(set) = self.block_meshes.get(&h) {
// The solid's own transparency (baked into the cached colour).
@ -4767,10 +5029,26 @@ impl Scene {
// 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, own_alpha) {
if let Some(c) =
self.block_mesh_override_color(e, h, inherit.as_ref(), own_alpha)
{
for lod in &mut ts.lods {
lod.color = c;
}
if let Some(material) = ts.material.as_mut() {
if material.handle.is_none() {
material.diffuse = c;
}
}
}
if let Some(material) =
self.block_mesh_override_material(e, inherit.as_ref())
{
material.apply_to_with_face_overrides(
&mut ts,
&self.document,
self.material_base_dir.as_deref(),
);
}
out.push(ts);
}
@ -4793,16 +5071,21 @@ impl Scene {
fn chain_mesh_inherit(
&self,
ins: &acadrust::entities::Insert,
parent_ins_color: [f32; 4],
parent_l0: [f32; 4],
) -> ([f32; 4], [f32; 4]) {
parent: &BlockMeshInherit,
) -> BlockMeshInherit {
use acadrust::types::Color;
let bg = self.current_bg();
let on_l0 = crate::scene::view::render::is_effective_layer_zero(&ins.common.layer);
let child_ins_color = if ins.common.color == Color::ByBlock {
parent_ins_color
} else if on_l0 && ins.common.color == Color::ByLayer {
parent_l0
let insert_entity = EntityType::Insert(ins.clone());
let has_book_color =
crate::scene::view::render::has_resolved_book_color(
&self.document,
&insert_entity,
);
let child_ins_color = if !has_book_color && ins.common.color == Color::ByBlock {
parent.insert_color
} else if !has_book_color && on_l0 && ins.common.color == Color::ByLayer {
parent.layer0_color
} else {
crate::scene::view::render::adapt_to_bg(
crate::scene::view::render::render_style_for(
@ -4814,7 +5097,7 @@ impl Scene {
)
};
let child_l0 = if on_l0 {
parent_l0
parent.layer0_color
} else {
crate::scene::view::render::adapt_to_bg(
crate::scene::view::render::layer_render_style(&self.document, &ins.common.layer)
@ -4822,7 +5105,35 @@ impl Scene {
bg,
)
};
(child_ins_color, child_l0)
let child_insert_material = if ins.common.material_flags == 1 {
parent.insert_material.clone()
} else if on_l0 && ins.common.material_flags == 0 {
parent.layer0_material.clone()
} else {
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
&insert_entity,
child_ins_color,
Some(&parent.insert_material),
self.material_base_dir.as_deref(),
)
};
let child_l0_material = if on_l0 {
parent.layer0_material.clone()
} else {
crate::scene::model::material_model::resolve_layer_material_with_base(
&self.document,
&ins.common.layer,
child_l0,
self.material_base_dir.as_deref(),
)
};
BlockMeshInherit {
insert_color: child_ins_color,
layer0_color: child_l0,
insert_material: child_insert_material,
layer0_material: child_l0_material,
}
}
/// Per-instance colour override for a block-internal solid mesh: ByBlock →
@ -4833,19 +5144,26 @@ impl Scene {
&self,
e: &EntityType,
h: Handle,
inherit: Option<([f32; 4], [f32; 4])>,
inherit: Option<&BlockMeshInherit>,
own_alpha: f32,
) -> Option<[f32; 4]> {
let (ins_color, l0_color) = inherit?;
let inherit = inherit?;
use acadrust::types::Color;
let common = e.common();
let on_l0 = crate::scene::view::render::is_effective_layer_zero(&common.layer);
let mut c = if common.color == Color::ByBlock {
ins_color
} else if on_l0 && common.color == Color::ByLayer {
let has_book_color =
crate::scene::view::render::has_resolved_book_color(&self.document, e);
let mut c = if !has_book_color && common.color == Color::ByBlock {
inherit.insert_color
} else if !has_book_color && on_l0 && common.color == Color::ByLayer {
// Inherit the insert layer's RGB but keep the solid's own alpha,
// matching the wire/hatch path (render_style_for_block_sub).
[l0_color[0], l0_color[1], l0_color[2], own_alpha]
[
inherit.layer0_color[0],
inherit.layer0_color[1],
inherit.layer0_color[2],
own_alpha,
]
} else {
return None;
};
@ -4857,6 +5175,24 @@ impl Scene {
Some(c)
}
fn block_mesh_override_material(
&self,
entity: &EntityType,
inherit: Option<&BlockMeshInherit>,
) -> Option<crate::scene::model::material_model::MeshMaterial> {
let inherit = inherit?;
let common = entity.common();
if common.material_flags == 1 {
Some(inherit.insert_material.clone())
} else if common.material_flags == 0
&& crate::scene::view::render::is_effective_layer_zero(&common.layer)
{
Some(inherit.layer0_material.clone())
} else {
None
}
}
/// Hatches eligible for click / box / lasso hit-testing in the current
/// layout. Filters out block-internal source hatches (stored in
/// `self.hatches` at block-local coords for the block-defn position,
@ -5015,7 +5351,9 @@ impl Scene {
let mut hatch_memo: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
for entity in self.document.entities() {
let EntityType::Insert(ins) = entity else {
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, entity);
let EntityType::Insert(ins) = contextual.as_ref() else {
continue;
};
if ins.common.invisible
@ -5037,6 +5375,13 @@ impl Scene {
for sub in ins
.explode_from_document(&self.document)
.into_iter()
.map(|sub| {
crate::scene::annotative::entity_for_active_context(
&self.document,
&sub,
)
.into_owned()
})
.map(crate::modules::draw::modify::explode::normalize_insert_entity)
{
let EntityType::Hatch(dxf) = sub else {
@ -5803,6 +6148,9 @@ impl Scene {
| Some(EntityType::Region(_))
| Some(EntityType::Body(_))
| Some(EntityType::Surface(_))
| Some(EntityType::Mesh(_))
| Some(EntityType::PolygonMesh(_))
| Some(EntityType::PolyfaceMesh(_))
)
})
}

View file

@ -0,0 +1,447 @@
use acadrust::objects::{Material, MaterialColor, MaterialMap, ObjectType};
use acadrust::{CadDocument, EntityType, Handle};
use std::path::Path;
use std::sync::Arc;
/// Render-ready subset of an AcDbMaterial.
///
/// The complete decoded object stays authoritative in `CadDocument`; this
/// compact copy travels with a tessellated mesh so the GPU path does not need
/// to look back into the document. Map metadata is retained even when the
/// referenced image cannot be resolved, allowing a later texture upload to
/// replace the colour fallback without re-tessellating the solid.
#[derive(Clone, Debug)]
pub struct MeshMaterial {
pub handle: Option<Handle>,
pub name: String,
pub description: String,
pub diffuse: [f32; 4],
pub ambient: [f32; 3],
pub specular: [f32; 3],
pub gloss: f32,
pub reflectivity: f32,
pub self_illumination: f32,
pub translucence: f32,
pub refraction_index: f32,
pub luminance: f32,
pub two_sided: bool,
pub illumination_model: i32,
pub channel_flags: i32,
pub mode: i32,
pub indirect_bump_scale: f32,
pub reflectance_scale: f32,
pub transmittance_scale: f32,
pub luminance_mode: i16,
pub normal_map_method: i16,
pub normal_map_strength: f32,
pub is_anonymous: bool,
pub global_illumination: i16,
pub final_gather: i16,
pub color_bleed_scale: f32,
pub advanced_data_present: bool,
pub diffuse_map: MeshTextureMap,
pub specular_map: MeshTextureMap,
pub reflection_map: MeshTextureMap,
pub opacity_map: MeshTextureMap,
pub bump_map: MeshTextureMap,
pub refraction_map: MeshTextureMap,
pub normal_map: MeshTextureMap,
}
#[derive(Clone, Debug)]
pub struct MeshTextureMap {
pub blend_factor: f32,
pub projection: u8,
pub tiling: u8,
pub auto_transform: u8,
pub transform: [f32; 16],
pub source: u8,
pub file_name: String,
pub procedural: bool,
pub image: Option<Arc<MaterialImage>>,
}
#[derive(Clone, Debug)]
pub struct MaterialImage {
pub width: u32,
pub height: u32,
pub rgba: Arc<Vec<u8>>,
pub path: String,
}
impl Default for MeshTextureMap {
fn default() -> Self {
let mut transform = [0.0; 16];
transform[0] = 1.0;
transform[5] = 1.0;
transform[10] = 1.0;
transform[15] = 1.0;
Self {
blend_factor: 1.0,
projection: 2,
tiling: 1,
auto_transform: 1,
transform,
source: 0,
file_name: String::new(),
procedural: false,
image: None,
}
}
}
impl MeshTextureMap {
fn from_dwg(map: &MaterialMap, base_dir: Option<&Path>) -> Self {
let mut transform = [0.0; 16];
for (dst, src) in transform.iter_mut().zip(map.transform) {
*dst = src as f32;
}
Self {
blend_factor: map.blend_factor.clamp(0.0, 1.0) as f32,
projection: map.projection,
tiling: map.tiling,
auto_transform: map.auto_transform,
transform,
source: map.source,
file_name: map.file_name.clone(),
procedural: map.texture.is_some(),
image: load_map_image(map, base_dir)
.or_else(|| procedural_map_image(map)),
}
}
pub fn has_content(&self) -> bool {
(self.source == 1 && !self.file_name.trim().is_empty())
|| (self.source == 2 && self.procedural)
}
}
impl MeshMaterial {
pub fn entity_color(color: [f32; 4]) -> Self {
Self {
handle: None,
name: String::new(),
description: String::new(),
diffuse: color,
ambient: [color[0], color[1], color[2]],
specular: [1.0; 3],
gloss: 0.5,
reflectivity: 0.0,
self_illumination: 0.0,
translucence: 0.0,
refraction_index: 1.0,
luminance: 0.0,
two_sided: true,
illumination_model: 0,
channel_flags: 127,
mode: 0,
indirect_bump_scale: 1.0,
reflectance_scale: 1.0,
transmittance_scale: 1.0,
luminance_mode: 0,
normal_map_method: 0,
normal_map_strength: 1.0,
is_anonymous: false,
global_illumination: 0,
final_gather: 0,
color_bleed_scale: 1.0,
advanced_data_present: false,
diffuse_map: MeshTextureMap::default(),
specular_map: MeshTextureMap::default(),
reflection_map: MeshTextureMap::default(),
opacity_map: MeshTextureMap::default(),
bump_map: MeshTextureMap::default(),
refraction_map: MeshTextureMap::default(),
normal_map: MeshTextureMap::default(),
}
}
fn from_dwg(
handle: Handle,
material: &Material,
entity_color: [f32; 4],
base_dir: Option<&Path>,
) -> Self {
let diffuse_map = MeshTextureMap::from_dwg(&material.diffuse_map, base_dir);
let specular_map = MeshTextureMap::from_dwg(&material.specular_map, base_dir);
let reflection_map = MeshTextureMap::from_dwg(&material.reflection_map, base_dir);
let opacity_map = MeshTextureMap::from_dwg(&material.opacity_map, base_dir);
let bump_map = MeshTextureMap::from_dwg(&material.bump_map, base_dir);
let refraction_map = MeshTextureMap::from_dwg(&material.refraction_map, base_dir);
let 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)
};
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;
Self {
handle: Some(handle),
name: material.name.clone(),
description: material.description.clone(),
diffuse: [
diffuse[0],
diffuse[1],
diffuse[2],
entity_color[3] * opacity,
],
ambient: [ambient[0], ambient[1], ambient[2]],
specular: [specular[0], specular[1], specular[2]],
gloss: material.specular_gloss_factor.clamp(0.0, 1.0) as f32,
reflectivity: material.reflectivity.clamp(0.0, 1.0) as f32,
self_illumination: material.self_illumination.clamp(0.0, 1.0) as f32,
translucence: material.translucence.clamp(0.0, 1.0) as f32,
refraction_index: material.refraction_index.max(0.0) as f32,
luminance: material.luminance.max(0.0) as f32,
two_sided: material.two_sided_material,
illumination_model: material.illumination_model,
channel_flags: material.channel_flags,
mode: material.mode,
indirect_bump_scale: material.indirect_bump_scale as f32,
reflectance_scale: material.reflectance_scale as f32,
transmittance_scale: material.transmittance_scale as f32,
luminance_mode: material.luminance_mode,
normal_map_method: material.normal_map_method,
normal_map_strength: material.normal_map_strength.max(0.0) as f32,
is_anonymous: material.is_anonymous,
global_illumination: material.global_illumination,
final_gather: material.final_gather,
color_bleed_scale: material.color_bleed_scale as f32,
advanced_data_present: material.advanced_data_present,
diffuse_map,
specular_map,
reflection_map,
opacity_map,
bump_map,
refraction_map,
normal_map,
}
}
pub fn apply_to(&self, set: &mut super::mesh_model::MeshLodSet) {
for lod in &mut set.lods {
lod.color = self.diffuse;
}
set.material = Some(self.clone());
}
pub fn apply_to_with_face_overrides(
&self,
set: &mut super::mesh_model::MeshLodSet,
document: &CadDocument,
base_dir: Option<&Path>,
) {
self.apply_to(set);
set.face_materials.clear();
let handles: rustc_hash::FxHashSet<Handle> = set
.lods
.iter()
.flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied())
.collect();
for handle in handles {
let material =
resolve_material_handle_with_base(document, handle, self, base_dir);
set.face_materials.insert(handle, material);
}
}
}
fn material_color(value: &MaterialColor, current: [f32; 4]) -> [f32; 4] {
let base = if value.flag == 1 {
value.rgb.map_or(current, |packed| {
let packed = packed as u32;
[
((packed >> 16) & 0xff) as f32 / 255.0,
((packed >> 8) & 0xff) as f32 / 255.0,
(packed & 0xff) as f32 / 255.0,
current[3],
]
})
} else {
current
};
let factor = value.factor.clamp(0.0, 1.0) as f32;
[
base[0] * factor,
base[1] * factor,
base[2] * factor,
base[3],
]
}
fn procedural_map_image(map: &MaterialMap) -> Option<Arc<MaterialImage>> {
let texture = map.texture.as_ref()?;
let color1 = material_color(&texture.color1, [0.2, 0.2, 0.2, 1.0]);
let color2 = material_color(&texture.color2, [0.8, 0.8, 0.8, 1.0]);
let size = 64u32;
let mut rgba = Vec::with_capacity((size * size * 4) as usize);
for y in 0..size {
for x in 0..size {
let color = if ((x / 8) + (y / 8)) & 1 == 0 {
color1
} else {
color2
};
rgba.extend(color.map(|channel| {
(channel.clamp(0.0, 1.0) * 255.0).round() as u8
}));
}
}
Some(Arc::new(MaterialImage {
width: size,
height: size,
rgba: Arc::new(rgba),
path: format!("procedural:{}", texture.mode),
}))
}
/// Resolve an entity's effective material using AcDbEntity's material flags.
///
/// `by_block` supplies the enclosing INSERT's material for a block child.
/// Outside a block a ByBlock material has no owner source and falls back to
/// the entity colour, matching the existing ByBlock colour behaviour.
pub fn resolve_material_with_base(
document: &CadDocument,
entity: &EntityType,
entity_color: [f32; 4],
by_block: Option<&MeshMaterial>,
base_dir: Option<&Path>,
) -> MeshMaterial {
let common = entity.common();
if common.material_flags == 1 {
return by_block
.cloned()
.unwrap_or_else(|| MeshMaterial::entity_color(entity_color));
}
let handle = if common.material_flags == 3 {
common.material_handle
} else {
document
.layers
.get(&common.layer)
.map(|layer| layer.material)
.filter(|handle| handle.is_valid())
};
let Some(handle) = handle else {
return MeshMaterial::entity_color(entity_color);
};
match document.objects.get(&handle) {
Some(ObjectType::Material(material))
if material.name.eq_ignore_ascii_case("ByLayer")
|| material.name.eq_ignore_ascii_case("ByBlock") =>
{
MeshMaterial::entity_color(entity_color)
}
Some(ObjectType::Material(material)) => {
MeshMaterial::from_dwg(handle, material, entity_color, base_dir)
}
_ => MeshMaterial::entity_color(entity_color),
}
}
pub fn resolve_layer_material_with_base(
document: &CadDocument,
layer_name: &str,
layer_color: [f32; 4],
base_dir: Option<&Path>,
) -> MeshMaterial {
let handle = document
.layers
.get(layer_name)
.map(|layer| layer.material)
.filter(|handle| handle.is_valid());
match handle.and_then(|handle| document.objects.get(&handle).map(|object| (handle, object))) {
Some((_, ObjectType::Material(material)))
if material.name.eq_ignore_ascii_case("ByLayer")
|| material.name.eq_ignore_ascii_case("ByBlock") =>
{
MeshMaterial::entity_color(layer_color)
}
Some((handle, ObjectType::Material(material))) => {
MeshMaterial::from_dwg(handle, material, layer_color, base_dir)
}
_ => MeshMaterial::entity_color(layer_color),
}
}
pub fn resolve_material_handle_with_base(
document: &CadDocument,
handle: Handle,
fallback: &MeshMaterial,
base_dir: Option<&Path>,
) -> MeshMaterial {
match document.objects.get(&handle) {
Some(ObjectType::Material(material)) if material.name.eq_ignore_ascii_case("ByLayer") => {
fallback.clone()
}
Some(ObjectType::Material(material)) if material.name.eq_ignore_ascii_case("ByBlock") => {
fallback.clone()
}
Some(ObjectType::Material(material)) => {
MeshMaterial::from_dwg(handle, material, fallback.diffuse, base_dir)
}
_ => fallback.clone(),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn load_map_image(map: &MaterialMap, base_dir: Option<&Path>) -> Option<Arc<MaterialImage>> {
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<HashMap<PathBuf, Option<Arc<MaterialImage>>>>> = OnceLock::new();
if map.source != 1 || map.file_name.trim().is_empty() {
return None;
}
let normalized = map.file_name.trim().replace('\\', "/");
let source = PathBuf::from(&normalized);
let mut candidates = Vec::with_capacity(4);
if source.is_absolute() {
candidates.push(source.clone());
}
if let Some(base) = base_dir {
candidates.push(base.join(&source));
if let Some(name) = source.file_name() {
candidates.push(base.join(name));
for folder in ["Textures", "textures", "Materials", "materials"] {
candidates.push(base.join(folder).join(name));
}
}
}
let path = candidates.into_iter().find(|path| path.is_file())?;
let path = path.canonicalize().unwrap_or(path);
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(hit) = cache.lock().ok()?.get(&path).cloned() {
return hit;
}
let loaded = std::fs::read(&path)
.ok()
.and_then(|bytes| image::load_from_memory(&bytes).ok())
.map(|image| {
let rgba = image.to_rgba8();
Arc::new(MaterialImage {
width: rgba.width(),
height: rgba.height(),
rgba: Arc::new(rgba.into_raw()),
path: path.to_string_lossy().into_owned(),
})
});
cache.lock().ok()?.insert(path, loaded.clone());
loaded
}
#[cfg(target_arch = "wasm32")]
fn load_map_image(_map: &MaterialMap, _base_dir: Option<&Path>) -> Option<Arc<MaterialImage>> {
None
}

View file

@ -18,6 +18,12 @@ pub struct MeshModel {
pub normals: Vec<[f32; 3]>,
/// Triangle indices into `verts` (every 3 values = one triangle).
pub indices: Vec<u32>,
/// Optional AcDbMaterial handle per triangle. Empty means the whole mesh
/// uses `MeshLodSet::material`; otherwise each entry aligns with one
/// `indices` triplet and overrides the entity material for that face.
pub triangle_material_handles: Vec<Option<acadrust::Handle>>,
/// Optional ACIS face colour per triangle, aligned with `indices` triplets.
pub triangle_colors: Vec<Option<[f32; 4]>>,
/// RGBA colour in [0, 1].
pub color: [f32; 4],
/// Whether this mesh is currently selected.
@ -94,9 +100,41 @@ pub enum CurvedGen {
},
}
#[derive(Clone, Debug)]
pub struct StoredSilhouette {
pub viewport_id: i64,
pub view_direction: [f32; 3],
pub up_vector: [f32; 3],
pub target: [f32; 3],
pub is_perspective: bool,
pub edge_verts: Vec<[f32; 3]>,
pub edge_verts_low: Vec<[f32; 3]>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MeshMetrics {
pub vertices: usize,
pub triangles: usize,
pub surface_area: f64,
pub volume: f64,
pub centroid: [f64; 3],
}
#[derive(Clone, Debug)]
pub struct MeshLodSet {
pub lods: Vec<MeshModel>,
/// Effective AcDbMaterial resolved from entity/layer/INSERT inheritance.
/// Geometry remains usable when it is absent; the renderer then keeps the
/// per-mesh entity colour.
pub material: Option<super::material_model::MeshMaterial>,
/// Face-level ACIS material overrides, resolved from each LOD's
/// `triangle_material_handles`. Only handles actually referenced by the
/// tessellation are retained.
pub face_materials:
rustc_hash::FxHashMap<acadrust::Handle, super::material_model::MeshMaterial>,
/// Effective AcDbVisualStyle override resolved from the entity's full,
/// face and edge style handles.
pub visual_style: Option<super::visual_style_model::MeshVisualStyle>,
/// True only when every source face produced triangles. False keeps
/// downstream solid-edit code from treating a display-only partial shell
/// as a closed, valid solid.
@ -111,6 +149,14 @@ pub struct MeshLodSet {
/// Curved-face generators for per-frame silhouette (DISPSILH). Empty for a
/// solid with no curved faces, or when silhouettes aren't wanted.
pub curved_gens: Vec<CurvedGen>,
/// View-specific silhouette caches stored in COMMON_3DSOLID. They are used
/// when the decoded surface family cannot provide a live analytic
/// silhouette for the current view.
pub stored_silhouettes: Vec<StoredSilhouette>,
/// Geometry measurements calculated once from the highest available LOD.
/// Properties can read these without re-parsing or re-tessellating ACIS on
/// the UI thread.
pub metrics: MeshMetrics,
/// World XY AABB `[min_x, min_y, max_x, max_y]` of the mesh — used
/// by the per-frame LOD selector to compute the projected pixel
/// diagonal.
@ -144,16 +190,93 @@ pub fn compute_mesh_aabb(lods: &[MeshModel]) -> ([f32; 4], [f32; 2]) {
([min_x, min_y, max_x, max_y], [min_z, max_z])
}
fn compute_mesh_metrics(lods: &[MeshModel]) -> MeshMetrics {
let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
return MeshMetrics::default();
};
let point = |index: u32| {
let index = index as usize;
let high = mesh.verts.get(index).copied().unwrap_or([0.0; 3]);
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
[
high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
high[2] as f64 + low[2] as f64,
]
};
let mut area = 0.0;
let mut area_centroid_numerator = [0.0; 3];
let mut signed_volume = 0.0;
let mut centroid_numerator = [0.0; 3];
for triangle in mesh.indices.chunks_exact(3) {
let a = point(triangle[0]);
let b = point(triangle[1]);
let c = point(triangle[2]);
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let triangle_area = 0.5
* (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2])
.sqrt();
area += triangle_area;
for axis in 0..3 {
area_centroid_numerator[axis] +=
triangle_area * (a[axis] + b[axis] + c[axis]) / 3.0;
}
let tetra = (
a[0] * (b[1] * c[2] - b[2] * c[1])
- a[1] * (b[0] * c[2] - b[2] * c[0])
+ a[2] * (b[0] * c[1] - b[1] * c[0])
) / 6.0;
signed_volume += tetra;
for axis in 0..3 {
centroid_numerator[axis] += tetra * (a[axis] + b[axis] + c[axis]) / 4.0;
}
}
let centroid = if signed_volume.abs() > 1e-18 {
[
centroid_numerator[0] / signed_volume,
centroid_numerator[1] / signed_volume,
centroid_numerator[2] / signed_volume,
]
} else if area > 1e-18 {
[
area_centroid_numerator[0] / area,
area_centroid_numerator[1] / area,
area_centroid_numerator[2] / area,
]
} else {
[0.0; 3]
};
MeshMetrics {
vertices: mesh.verts.len(),
triangles: mesh.indices.len() / 3,
surface_area: area,
volume: signed_volume.abs(),
centroid,
}
}
impl MeshLodSet {
/// Build a set from its LODs, computing the 3D AABB.
pub fn from_lods(lods: Vec<MeshModel>) -> Self {
let (world_aabb, z_aabb) = compute_mesh_aabb(&lods);
let metrics = compute_mesh_metrics(&lods);
Self {
lods,
material: None,
face_materials: rustc_hash::FxHashMap::default(),
visual_style: None,
complete: true,
edge_verts: Vec::new(),
edge_verts_low: Vec::new(),
curved_gens: Vec::new(),
stored_silhouettes: Vec::new(),
metrics,
world_aabb,
z_aabb,
}

View file

@ -3,8 +3,10 @@ pub mod gdi;
pub mod hatch_model;
pub mod hatch_patterns;
pub mod image_model;
pub mod material_model;
pub mod ole_pres;
pub mod pdf_raster;
pub mod mesh_model;
pub mod solid_model;
pub mod visual_style_model;
pub mod object;

View file

@ -180,6 +180,8 @@ pub fn mesh_from_solid(solid: &Solid, color: [f32; 4]) -> Option<MeshLodSet> {
verts_low,
normals,
indices,
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color,
selected: false,
};

View file

@ -0,0 +1,278 @@
use acadrust::objects::{
ObjectType, VisualStyle, VisualStyleProperty, VisualStylePropertyValue,
};
use acadrust::{CadDocument, EntityType, Handle};
#[derive(Clone, Debug)]
pub struct MeshVisualStyle {
pub full_handle: Option<Handle>,
pub face_handle: Option<Handle>,
pub edge_handle: Option<Handle>,
pub description: String,
pub style_type: i16,
pub face_lighting_model: i16,
pub face_lighting_quality: i16,
pub face_color_mode: i16,
pub face_modifier: i32,
pub face_opacity: f32,
pub face_specular: f32,
pub mono_color: Option<[f32; 4]>,
pub edge_model: i32,
pub edge_style: i32,
pub edge_color: Option<[f32; 4]>,
pub edge_opacity: f32,
pub edge_width: f32,
pub edge_overhang: i32,
pub edge_jitter: i32,
pub silhouette_color: Option<[f32; 4]>,
pub silhouette_width: i32,
pub halo_gap: i32,
pub isoline_count: i32,
pub hide_precision: bool,
pub display_settings: i32,
pub brightness: f32,
pub shadow_type: i32,
pub extended_lighting_model: i16,
pub internal_use_only: bool,
}
fn core_property(style: &VisualStyle, index: usize) -> Option<&VisualStyleProperty> {
if style.properties.len() >= 28 {
return style.properties.get(index);
}
if style.properties.len() == 24 {
let legacy = match index {
4 => 0,
5 => 1,
6 => 2,
9 => 3,
10 => 4,
11 => 5,
13 => 6,
14 => 7,
15 => 8,
16 => 9,
17 => 10,
18 => 11,
19 => 12,
20 => 13,
21 => 14,
22 => 15,
23 => 16,
24 => 17,
12 => 19,
25 => 20,
26 => 21,
27 => 22,
_ => return None,
};
return style.properties.get(legacy);
}
style.properties.get(index)
}
fn property_double(style: &VisualStyle, index: usize, fallback: f64) -> f64 {
let Some(property) = core_property(style, index) else {
return fallback;
};
match property.value {
VisualStylePropertyValue::Short(value) => value as f64,
VisualStylePropertyValue::Long(value) => value as f64,
VisualStylePropertyValue::Double(value) => value,
VisualStylePropertyValue::Bool(value) => value as u8 as f64,
_ => fallback,
}
}
fn property_long(style: &VisualStyle, index: usize, fallback: i32) -> i32 {
property_double(style, index, fallback as f64) as i32
}
fn property_bool(style: &VisualStyle, index: usize, fallback: bool) -> bool {
property_double(style, index, fallback as u8 as f64) != 0.0
}
fn property_color(style: &VisualStyle, index: usize) -> Option<[f32; 4]> {
let property = core_property(style, index)?;
if property.enabled == 0 {
return None;
}
let VisualStylePropertyValue::Color(color) = &property.value else {
return None;
};
if matches!(
color,
acadrust::types::Color::ByLayer | acadrust::types::Color::ByBlock
) {
return None;
}
Some(crate::scene::convert::tess_util::aci_to_rgba(color))
}
impl MeshVisualStyle {
fn from_dwg(handle: Handle, style: &VisualStyle) -> Self {
Self {
full_handle: Some(handle),
face_handle: None,
edge_handle: None,
description: style.description.clone(),
style_type: style.style_type,
face_lighting_model: style.face_lighting_model,
face_lighting_quality: style.face_lighting_quality,
face_color_mode: style.face_color_mode,
face_modifier: style.face_modifier,
face_opacity: property_double(style, 4, 1.0).clamp(0.0, 1.0) as f32,
face_specular: property_double(style, 5, 0.0).max(0.0) as f32,
mono_color: property_color(style, 6),
edge_model: style.edge_model,
edge_style: style.edge_style,
edge_color: property_color(style, 15),
edge_opacity: property_double(style, 16, 1.0).clamp(0.0, 1.0) as f32,
edge_width: property_double(style, 17, 1.0).max(0.0) as f32,
edge_overhang: property_long(style, 18, 0),
edge_jitter: property_long(style, 19, 0),
silhouette_color: property_color(style, 20),
silhouette_width: property_long(style, 21, 0),
halo_gap: property_long(style, 22, 0),
isoline_count: property_long(style, 23, 0),
hide_precision: property_bool(style, 24, false),
display_settings: property_long(style, 25, 0),
brightness: property_double(style, 26, 0.0) as f32,
shadow_type: if style.properties.len() == 24 {
style
.properties
.get(23)
.map_or(0, |property| match property.value {
VisualStylePropertyValue::Short(value) => value as i32,
VisualStylePropertyValue::Long(value) => value,
VisualStylePropertyValue::Double(value) => value as i32,
VisualStylePropertyValue::Bool(value) => value as i32,
_ => 0,
})
} else {
property_long(style, 27, 0)
},
extended_lighting_model: style.extended_lighting_model,
internal_use_only: style.internal_use_only,
}
}
fn override_face(&mut self, handle: Handle, style: &VisualStyle) {
let source = Self::from_dwg(handle, style);
self.face_handle = Some(handle);
self.face_lighting_model = source.face_lighting_model;
self.face_lighting_quality = source.face_lighting_quality;
self.face_color_mode = source.face_color_mode;
self.face_modifier = source.face_modifier;
self.face_opacity = source.face_opacity;
self.face_specular = source.face_specular;
self.mono_color = source.mono_color;
self.brightness = source.brightness;
self.extended_lighting_model = source.extended_lighting_model;
}
fn override_edge(&mut self, handle: Handle, style: &VisualStyle) {
let source = Self::from_dwg(handle, style);
self.edge_handle = Some(handle);
self.edge_model = source.edge_model;
self.edge_style = source.edge_style;
self.edge_color = source.edge_color;
self.edge_opacity = source.edge_opacity;
self.edge_width = source.edge_width;
self.edge_overhang = source.edge_overhang;
self.edge_jitter = source.edge_jitter;
self.silhouette_color = source.silhouette_color;
self.silhouette_width = source.silhouette_width;
self.halo_gap = source.halo_gap;
self.isoline_count = source.isoline_count;
self.hide_precision = source.hide_precision;
}
pub fn face_visible(&self) -> bool {
self.face_lighting_model != 0 && self.face_color_mode != 0
}
pub fn edges_visible(&self) -> bool {
self.edge_model != 0
}
pub fn face_color(&self, base: [f32; 4]) -> [f32; 4] {
let mut color = match self.face_color_mode {
3 | 4 => self.mono_color.unwrap_or(base),
5 => {
let tint = self.mono_color.unwrap_or(base);
[
(base[0] + tint[0]) * 0.5,
(base[1] + tint[1]) * 0.5,
(base[2] + tint[2]) * 0.5,
base[3],
]
}
6 => {
let luminance = base[0] * 0.2126 + base[1] * 0.7152 + base[2] * 0.0722;
[
base[0] * 0.7 + luminance * 0.3,
base[1] * 0.7 + luminance * 0.3,
base[2] * 0.7 + luminance * 0.3,
base[3],
]
}
_ => base,
};
if self.face_modifier & 1 != 0 {
color[3] *= self.face_opacity;
}
color
}
pub fn edge_color(&self, base: [f32; 4]) -> [f32; 4] {
let mut color = self.edge_color.unwrap_or(base);
color[3] *= self.edge_opacity;
color
}
}
fn visual_style(
document: &CadDocument,
handle: Option<Handle>,
) -> Option<(Handle, &VisualStyle)> {
let handle = handle.filter(|handle| handle.is_valid())?;
match document.objects.get(&handle) {
Some(ObjectType::VisualStyle(style)) => Some((handle, style)),
_ => None,
}
}
pub fn resolve_mesh_visual_style(
document: &CadDocument,
entity: &EntityType,
) -> Option<MeshVisualStyle> {
let common = entity.common();
let full = visual_style(document, common.full_visual_style_handle);
let face = visual_style(document, common.face_visual_style_handle);
let edge = visual_style(document, common.edge_visual_style_handle);
let mut result = full
.map(|(handle, style)| MeshVisualStyle::from_dwg(handle, style))
.or_else(|| {
face.or(edge)
.map(|(handle, style)| MeshVisualStyle::from_dwg(handle, style))
})?;
if full.is_none() {
result.full_handle = None;
}
if let Some((handle, style)) = face {
result.override_face(handle, style);
}
if let Some((handle, style)) = edge {
result.override_edge(handle, style);
}
Some(result)
}
pub fn apply_mesh_visual_style(
set: &mut super::mesh_model::MeshLodSet,
document: &CadDocument,
entity: &EntityType,
) {
set.visual_style = resolve_mesh_visual_style(document, entity);
}

View file

@ -200,6 +200,14 @@ impl Scene {
}
}
}
for &h in handles {
if crate::scene::annotative::sync_active_context_from_entity(
&mut self.document,
h,
) {
self.poison_undo_recording();
}
}
// Move the baked dimension-block sub-entities too (collected above).
// These are entities not named in the reported change set, so a delta
// must capture their before-images here or a dimension move won't undo.
@ -438,6 +446,12 @@ impl Scene {
if let Some(entity) = self.document.get_entity_mut(handle) {
view::dispatch::apply_grip(entity, grip_id, apply);
}
if crate::scene::annotative::sync_active_context_from_entity(
&mut self.document,
handle,
) {
self.poison_undo_recording();
}
// A dimension loaded from a file renders through its baked *D block;
// the grip only moved a definition point, so the baked graphics are
// stale — drop them so tessellation falls back to the live geometry
@ -452,21 +466,83 @@ impl Scene {
.and_then(crate::entities::solid3d::point_of_reference)
.map(|p| [p.x, p.y, p.z]);
if let Some(new) = new_por {
let dx = (new[0] - old[0]) as f32;
let dy = (new[1] - old[1]) as f32;
let dz = (new[2] - old[2]) as f32;
let delta = [new[0] - old[0], new[1] - old[1], new[2] - old[2]];
if let Some(set) = self.meshes.get_mut(&handle) {
let translate_split =
|high: &mut [f32; 3], low: &mut [f32; 3]| {
let absolute = [
high[0] as f64 + low[0] as f64 + delta[0],
high[1] as f64 + low[1] as f64 + delta[1],
high[2] as f64 + low[2] as f64 + delta[2],
];
*high = [
absolute[0] as f32,
absolute[1] as f32,
absolute[2] as f32,
];
*low = [
(absolute[0] - high[0] as f64) as f32,
(absolute[1] - high[1] as f64) as f32,
(absolute[2] - high[2] as f64) as f32,
];
};
for lod in &mut set.lods {
for v in &mut lod.verts {
v[0] += dx;
v[1] += dy;
v[2] += dz;
if lod.verts_low.len() != lod.verts.len() {
lod.verts_low = vec![[0.0; 3]; lod.verts.len()];
}
for (high, low) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) {
translate_split(high, low);
}
}
set.world_aabb[0] += dx;
set.world_aabb[1] += dy;
set.world_aabb[2] += dx;
set.world_aabb[3] += dy;
if set.edge_verts_low.len() != set.edge_verts.len() {
set.edge_verts_low = vec![[0.0; 3]; set.edge_verts.len()];
}
for (high, low) in set
.edge_verts
.iter_mut()
.zip(set.edge_verts_low.iter_mut())
{
translate_split(high, low);
}
for generator in &mut set.curved_gens {
match generator {
crate::scene::model::mesh_model::CurvedGen::Cone {
base,
base_low,
..
} => translate_split(base, base_low),
crate::scene::model::mesh_model::CurvedGen::Sphere {
center,
center_low,
..
}
| crate::scene::model::mesh_model::CurvedGen::Torus {
center,
center_low,
..
} => translate_split(center, center_low),
}
}
for silhouette in &mut set.stored_silhouettes {
silhouette.target[0] += delta[0] as f32;
silhouette.target[1] += delta[1] as f32;
silhouette.target[2] += delta[2] as f32;
if silhouette.edge_verts_low.len() != silhouette.edge_verts.len() {
silhouette.edge_verts_low =
vec![[0.0; 3]; silhouette.edge_verts.len()];
}
for (high, low) in silhouette
.edge_verts
.iter_mut()
.zip(silhouette.edge_verts_low.iter_mut())
{
translate_split(high, low);
}
}
set.metrics.centroid[0] += delta[0];
set.metrics.centroid[1] += delta[1];
set.metrics.centroid[2] += delta[2];
set.recompute_aabb();
}
}
}

View file

@ -310,9 +310,12 @@ impl Scene {
};
let mut models: Vec<HatchModel> = Vec::new();
for (&handle, model) in self.hatches.iter() {
let Some(entity) = self.document.get_entity(handle) else {
let Some(source) = self.document.get_entity(handle) else {
continue;
};
let contextual =
crate::scene::annotative::entity_for_active_context(&self.document, source);
let entity = contextual.as_ref();
let c = entity.common();
if c.invisible || layer_hidden(&c.layer) {
continue;
@ -320,7 +323,19 @@ impl Scene {
if !self.belongs_to_visible_block(handle, c.owner_handle, layout_block) {
continue;
}
let mut m = model.clone();
let mut m = match entity {
EntityType::Hatch(dxf)
if crate::scene::annotative::active_object_context(
&self.document,
handle,
)
.is_some() =>
{
Self::hatch_model_from_dxf(dxf, model.color)
.unwrap_or_else(|| model.clone())
}
_ => model.clone(),
};
m.color = self.render_style(entity).0;
if let EntityType::Hatch(dxf) = entity {
// Only re-apply pattern_scale/angle for catalog-derived patterns

View file

@ -20,6 +20,23 @@ pub struct MeshVertex {
pub normal: [f32; 3],
pub color: [f32; 4],
pub position_low: [f32; 3],
/// gloss, reflectivity, self-illumination, luminance
pub material: [f32; 4],
/// specular RGB and refraction index
pub specular: [f32; 4],
pub uv_diffuse: [f32; 2],
/// ambient RGB and translucence
pub ambient: [f32; 4],
/// normal strength, bump scale, reflectance scale, transmittance scale
pub advanced: [f32; 4],
/// illumination model, channel flags, material mode, luminance mode
pub flags: [u32; 4],
pub uv_specular: [f32; 2],
pub uv_reflection: [f32; 2],
pub uv_opacity: [f32; 2],
pub uv_bump: [f32; 2],
pub uv_refraction: [f32; 2],
pub uv_normal: [f32; 2],
}
impl MeshVertex {
@ -45,6 +62,66 @@ impl MeshVertex {
shader_location: 3,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, material) as u64,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, specular) as u64,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_diffuse) as u64,
shader_location: 6,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, ambient) as u64,
shader_location: 7,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, advanced) as u64,
shader_location: 8,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, flags) as u64,
shader_location: 9,
format: wgpu::VertexFormat::Uint32x4,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_specular) as u64,
shader_location: 10,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_reflection) as u64,
shader_location: 11,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_opacity) as u64,
shader_location: 12,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_bump) as u64,
shader_location: 13,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_refraction) as u64,
shader_location: 14,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: std::mem::offset_of!(MeshVertex, uv_normal) as u64,
shader_location: 15,
format: wgpu::VertexFormat::Float32x2,
},
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<MeshVertex>() as u64,
@ -128,6 +205,8 @@ pub struct MeshBatchChunk {
/// buffer (pairs of endpoints), drawn non-indexed. Empty for plain meshes.
pub edge_vertex_buffer: wgpu::Buffer,
pub edge_vertex_count: u32,
pub material: Option<crate::scene::model::material_model::MeshMaterial>,
pub material_bind_group: Option<wgpu::BindGroup>,
}
fn make_chunk(
@ -137,6 +216,7 @@ fn make_chunk(
transp_indices: &[u32],
wire_indices: &[u32],
edge_verts: &[MeshVertex],
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> MeshBatchChunk {
// `create_buffer_init` with an empty slice yields a zero-sized buffer that
// some backends reject for INDEX usage; a chunk can legitimately hold only
@ -157,6 +237,18 @@ fn make_chunk(
normal: [0.0, 1.0, 0.0],
color: [0.0; 4],
position_low: [0.0; 3],
material: [0.0; 4],
specular: [1.0, 1.0, 1.0, 1.0],
uv_diffuse: [0.0; 2],
ambient: [0.3, 0.3, 0.3, 0.0],
advanced: [1.0; 4],
flags: [0, 127, 0, 0],
uv_specular: [0.0; 2],
uv_reflection: [0.0; 2],
uv_opacity: [0.0; 2],
uv_bump: [0.0; 2],
uv_refraction: [0.0; 2],
uv_normal: [0.0; 2],
}];
let src = if data.is_empty() { &stub[..] } else { data };
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@ -175,9 +267,411 @@ fn make_chunk(
wire_index_count: wire_indices.len() as u32,
edge_vertex_buffer: mk_vertex(edge_verts, "mesh.batch.edge_vbuf"),
edge_vertex_count: edge_verts.len() as u32,
material: material.cloned(),
material_bind_group: None,
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct MaterialMapParams {
/// diffuse, specular, reflection and opacity blend factors.
blends0: [f32; 4],
/// Presence bits for the same four maps.
present0: [u32; 4],
/// bump, refraction, normal and reserved blend factors.
blends1: [f32; 4],
/// Presence bits for the same four maps.
present1: [u32; 4],
}
fn upload_rgba_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &'static str,
image: Option<&crate::scene::model::material_model::MaterialImage>,
fallback: [u8; 4],
srgb: bool,
) -> wgpu::TextureView {
let (width, height, pixels) = image.map_or((1, 1, fallback.as_slice()), |image| {
(image.width, image.height, image.rgba.as_slice())
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: if srgb {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
texture.create_view(&wgpu::TextureViewDescriptor::default())
}
pub fn create_material_bind_group(
device: &wgpu::Device,
queue: &wgpu::Queue,
layout: &wgpu::BindGroupLayout,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> wgpu::BindGroup {
let diffuse = material.and_then(|material| material.diffuse_map.image.as_deref());
let specular = material.and_then(|material| material.specular_map.image.as_deref());
let reflection = material.and_then(|material| material.reflection_map.image.as_deref());
let opacity = material.and_then(|material| material.opacity_map.image.as_deref());
let bump = material.and_then(|material| material.bump_map.image.as_deref());
let refraction = material.and_then(|material| material.refraction_map.image.as_deref());
let normal = material.and_then(|material| material.normal_map.image.as_deref());
let diffuse_view =
upload_rgba_texture(device, queue, "mesh.material.diffuse", diffuse, [255; 4], true);
let specular_view =
upload_rgba_texture(device, queue, "mesh.material.specular", specular, [255; 4], true);
let reflection_view =
upload_rgba_texture(device, queue, "mesh.material.reflection", reflection, [0, 0, 0, 255], true);
let opacity_view =
upload_rgba_texture(device, queue, "mesh.material.opacity", opacity, [255; 4], false);
let bump_view =
upload_rgba_texture(device, queue, "mesh.material.bump", bump, [128, 128, 128, 255], false);
let refraction_view =
upload_rgba_texture(device, queue, "mesh.material.refraction", refraction, [255; 4], true);
let normal_view = upload_rgba_texture(
device,
queue,
"mesh.material.normal",
normal,
[128, 128, 255, 255],
false,
);
let sampler = |label: &'static str, tiling: u8| {
let address = match tiling {
1 => wgpu::AddressMode::Repeat,
4 => wgpu::AddressMode::MirrorRepeat,
_ => wgpu::AddressMode::ClampToEdge,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(label),
address_mode_u: address,
address_mode_v: address,
address_mode_w: address,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
})
};
let tiling = |channel: usize| {
material.map_or(1, |material| {
[
material.diffuse_map.tiling,
material.specular_map.tiling,
material.reflection_map.tiling,
material.opacity_map.tiling,
material.bump_map.tiling,
material.refraction_map.tiling,
material.normal_map.tiling,
][channel]
})
};
let diffuse_sampler = sampler("mesh.material.diffuse_sampler", tiling(0));
let specular_sampler = sampler("mesh.material.specular_sampler", tiling(1));
let reflection_sampler = sampler("mesh.material.reflection_sampler", tiling(2));
let opacity_sampler = sampler("mesh.material.opacity_sampler", tiling(3));
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 params = MaterialMapParams {
blends0: material.map_or([0.0; 4], |material| {
[
material.diffuse_map.blend_factor,
material.specular_map.blend_factor,
material.reflection_map.blend_factor,
material.opacity_map.blend_factor,
]
}),
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,
]
}),
blends1: material.map_or([0.0; 4], |material| {
[
material.bump_map.blend_factor,
material.refraction_map.blend_factor,
material.normal_map.blend_factor,
0.0,
]
}),
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,
0,
]
}),
};
let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("mesh.material.params"),
contents: bytemuck::bytes_of(&params),
usage: wgpu::BufferUsages::UNIFORM,
});
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mesh.material.bind_group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&specular_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&reflection_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&opacity_view),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(&bump_view),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&refraction_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::TextureView(&normal_view),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::Sampler(&specular_sampler),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::Sampler(&reflection_sampler),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::Sampler(&opacity_sampler),
},
wgpu::BindGroupEntry {
binding: 11,
resource: wgpu::BindingResource::Sampler(&bump_sampler),
},
wgpu::BindGroupEntry {
binding: 12,
resource: wgpu::BindingResource::Sampler(&refraction_sampler),
},
wgpu::BindGroupEntry {
binding: 13,
resource: wgpu::BindingResource::Sampler(&normal_sampler),
},
wgpu::BindGroupEntry {
binding: 14,
resource: params_buffer.as_entire_binding(),
},
],
})
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct MaterialBatchKey([u32; 16]);
fn material_key(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
) -> MaterialBatchKey {
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,
]);
};
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,
])
}
struct MeshBatchPart<'a> {
set: &'a MeshLodSet,
mesh: &'a MeshModel,
material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
indices: Vec<u32>,
include_faces: bool,
include_edges: bool,
}
fn material_map_uv(
map: &crate::scene::model::material_model::MeshTextureMap,
position: [f32; 3],
normal: [f32; 3],
) -> [f32; 2] {
let m = &map.transform;
let p = [
position[0] * m[0] + position[1] * m[1] + position[2] * m[2] + m[3],
position[0] * m[4] + position[1] * m[5] + position[2] * m[6] + m[7],
position[0] * m[8] + position[1] * m[9] + position[2] * m[10] + m[11],
];
match map.projection {
3 => [
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
p[2],
],
4 => {
let radius = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
if radius <= f32::EPSILON {
[0.0; 2]
} else {
[
p[1].atan2(p[0]) / std::f32::consts::TAU + 0.5,
(p[2] / radius).clamp(-1.0, 1.0).acos() / std::f32::consts::PI,
]
}
}
2 => {
let n = [normal[0].abs(), normal[1].abs(), normal[2].abs()];
if n[0] >= n[1] && n[0] >= n[2] {
[p[1], p[2]]
} else if n[1] >= n[2] {
[p[0], p[2]]
} else {
[p[0], p[1]]
}
}
_ => [p[0], p[1]],
}
}
fn material_uvs(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
position: [f32; 3],
normal: [f32; 3],
) -> [[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),
]
}
fn material_vertex_params(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> ([f32; 4], [f32; 4], [f32; 4], [f32; 4], [u32; 4]) {
let Some(material) = material else {
return (
[0.5, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 1.0],
[0.3, 0.3, 0.3, 0.0],
[1.0; 4],
[0, 127, 0, 0],
);
};
(
[
material.gloss,
material.reflectivity,
material.self_illumination,
material.luminance,
],
[
material.specular[0],
material.specular[1],
material.specular[2],
material.refraction_index,
],
[
material.ambient[0],
material.ambient[1],
material.ambient[2],
material.translucence,
],
[
material.normal_map_strength,
material.indirect_bump_scale,
material.reflectance_scale,
material.transmittance_scale,
],
[
material.illumination_model as u32,
material.channel_flags as u32,
material.mode as u32,
material.luminance_mode as u32,
],
)
}
/// Concatenate every set's first non-empty LOD into a few large GPU buffers.
/// Returns the chunks plus the total triangle count drawn (for diagnostics).
///
@ -202,34 +696,181 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
let mut wire_indices: Vec<u32> = Vec::new();
let mut edge_verts: Vec<MeshVertex> = Vec::new();
let mut total_tris = 0u64;
let mut ordered: Vec<MeshBatchPart<'_>> = Vec::new();
for set in sets {
let Some(mesh) = set.lods.iter().find(|m| !m.indices.is_empty()) else {
let Some(mesh) = set.lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
continue;
};
let triangle_count = mesh.indices.len() / 3;
let has_face_materials = mesh.triangle_material_handles.len() == triangle_count
&& !set.face_materials.is_empty();
let has_face_colors = mesh.triangle_colors.len() == triangle_count
&& mesh.triangle_colors.iter().any(Option::is_some);
let include_faces = set
.visual_style
.as_ref()
.map_or(true, |style| style.face_visible());
let include_edges = set
.visual_style
.as_ref()
.map_or(true, |style| style.edges_visible());
if !has_face_materials && !has_face_colors {
let base_color =
set.material.as_ref().map_or(mesh.color, |material| material.diffuse);
let color = set
.visual_style
.as_ref()
.map_or(base_color, |style| style.face_color(base_color));
ordered.push(MeshBatchPart {
set,
mesh,
material: set.material.as_ref(),
color,
indices: mesh.indices.clone(),
include_faces,
include_edges,
});
continue;
}
let mut groups: std::collections::BTreeMap<
MaterialBatchKey,
(
Option<&crate::scene::model::material_model::MeshMaterial>,
[f32; 4],
Vec<u32>,
),
> = std::collections::BTreeMap::new();
for (triangle, indices) in mesh.indices.chunks_exact(3).enumerate() {
let material = if has_face_materials {
mesh.triangle_material_handles[triangle]
.and_then(|handle| set.face_materials.get(&handle))
.or(set.material.as_ref())
} else {
set.material.as_ref()
};
let base_color = material.map_or(mesh.color, |material| material.diffuse);
let base_color = if has_face_colors {
mesh.triangle_colors[triangle].unwrap_or(base_color)
} else {
base_color
};
let color = set
.visual_style
.as_ref()
.map_or(base_color, |style| style.face_color(base_color));
groups
.entry(material_key(material, color))
.or_insert_with(|| (material, color, Vec::new()))
.2
.extend_from_slice(indices);
}
for (part_index, (_, (material, color, indices))) in groups.into_iter().enumerate() {
ordered.push(MeshBatchPart {
set,
mesh,
material,
color,
indices,
include_faces,
include_edges: include_edges && part_index == 0,
});
}
}
ordered.sort_by_key(|part| {
material_key(part.material, part.color)
});
let mut active_key: Option<MaterialBatchKey> = None;
let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None;
for part in ordered {
let set = part.set;
let mesh = part.mesh;
let material = part.material;
let part_color = part.color;
let key = material_key(material, part_color);
if active_key.is_some_and(|active| active != key)
&& (!verts.is_empty() || !edge_verts.is_empty())
{
chunks.push(make_chunk(
device,
&verts,
&indices,
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
verts.clear();
indices.clear();
transp_indices.clear();
wire_indices.clear();
edge_verts.clear();
}
active_key = Some(key);
active_material = material;
let has_normals = mesh.normals.len() == mesh.verts.len();
let vtx = |vi: usize| MeshVertex {
position: mesh.verts[vi],
normal: if has_normals { mesh.normals[vi] } else { [0.0, 1.0, 0.0] },
color: mesh.color,
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let edge_color = set
.visual_style
.as_ref()
.map_or(mesh.color, |style| style.edge_color(mesh.color));
let vtx = |vi: usize| {
let normal = if has_normals {
mesh.normals[vi]
} else {
[0.0, 1.0, 0.0]
};
let uv = material_uvs(material, mesh.verts[vi], normal);
MeshVertex {
position: mesh.verts[vi],
normal,
color: part_color,
position_low: mesh.verts_low.get(vi).copied().unwrap_or([0.0; 3]),
material: material_params,
specular,
uv_diffuse: uv[0],
ambient,
advanced,
flags,
uv_specular: uv[1],
uv_reflection: uv[2],
uv_opacity: uv[3],
uv_bump: uv[4],
uv_refraction: uv[5],
uv_normal: uv[6],
}
};
// 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 = mesh.color[3] < 0.999;
let mesh_tris = mesh.indices.len() / 3;
total_tris += mesh_tris as u64;
let is_transp = part_color[3] < 0.999;
let mesh_tris = part.indices.len() / 3;
if part.include_faces {
total_tris += mesh_tris as u64;
}
// Feature edges present (ACIS solid) → emit the B-rep edges as a line
// list and skip the triangulation wireframe. Absent (plain mesh) → keep
// the triangle edges so the mesh still shows a wireframe.
let has_feat = !set.edge_verts.is_empty();
if has_feat {
if has_feat && part.include_edges {
for i in 0..set.edge_verts.len() {
edge_verts.push(MeshVertex {
position: set.edge_verts[i],
normal: [0.0, 1.0, 0.0],
color: mesh.color,
color: edge_color,
position_low: set.edge_verts_low.get(i).copied().unwrap_or([0.0; 3]),
material: material_params,
specular,
uv_diffuse: [0.0; 2],
ambient,
advanced,
flags,
uv_specular: [0.0; 2],
uv_reflection: [0.0; 2],
uv_opacity: [0.0; 2],
uv_bump: [0.0; 2],
uv_refraction: [0.0; 2],
uv_normal: [0.0; 2],
});
}
}
@ -245,6 +886,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
verts.clear();
indices.clear();
@ -258,22 +900,40 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
let end = (t + tris_per).min(mesh_tris);
let (mut sv, mut si, mut swi) = (Vec::new(), Vec::new(), Vec::new());
for tri in t..end {
let ix = &mesh.indices[tri * 3..tri * 3 + 3];
let ix = &part.indices[tri * 3..tri * 3 + 3];
let b = sv.len() as u32;
sv.push(vtx(ix[0] as usize));
sv.push(vtx(ix[1] as usize));
sv.push(vtx(ix[2] as usize));
si.extend_from_slice(&[b, b + 1, b + 2]);
if !has_feat {
if part.include_faces {
si.extend_from_slice(&[b, b + 1, b + 2]);
}
if part.include_edges && !has_feat {
swi.extend_from_slice(&[b, b + 1, b + 1, b + 2, b + 2, b]);
}
}
// The whole mesh shares one colour, so a sub-chunk is entirely
// opaque or entirely transparent.
if is_transp {
chunks.push(make_chunk(device, &sv, &[], &si, &swi, &[]));
chunks.push(make_chunk(
device,
&sv,
&[],
&si,
&swi,
&[],
active_material,
));
} else {
chunks.push(make_chunk(device, &sv, &si, &[], &swi, &[]));
chunks.push(make_chunk(
device,
&sv,
&si,
&[],
&swi,
&[],
active_material,
));
}
t = end;
}
@ -293,6 +953,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
verts.clear();
indices.clear();
@ -304,18 +965,24 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
for i in 0..mesh.verts.len() {
verts.push(vtx(i));
}
let fill = if is_transp { &mut transp_indices } else { &mut indices };
for &idx in &mesh.indices {
fill.push(base + idx);
if part.include_faces {
let fill = if is_transp { &mut transp_indices } else { &mut indices };
for &idx in &part.indices {
fill.push(base + idx);
}
}
if !has_feat {
for tri in mesh.indices.chunks_exact(3) {
if part.include_edges && !has_feat {
for tri in part.indices.chunks_exact(3) {
let (a, b, c) = (base + tri[0], base + tri[1], base + tri[2]);
wire_indices.extend_from_slice(&[a, b, b, c, c, a]);
}
}
}
if !indices.is_empty() || !transp_indices.is_empty() || !edge_verts.is_empty() {
if !indices.is_empty()
|| !transp_indices.is_empty()
|| !wire_indices.is_empty()
|| !edge_verts.is_empty()
{
chunks.push(make_chunk(
device,
&verts,
@ -323,6 +990,7 @@ pub fn build_mesh_batch(device: &wgpu::Device, sets: &[MeshLodSet]) -> (Vec<Mesh
&transp_indices,
&wire_indices,
&edge_verts,
active_material,
));
}
(chunks, total_tris)
@ -336,7 +1004,7 @@ impl MeshLodGpu {
.lods
.iter()
.filter(|m| !m.indices.is_empty())
.map(|m| MeshGpu::new(device, m, highlight))
.map(|m| MeshGpu::new(device, m, set.material.as_ref(), highlight))
.collect(),
world_aabb: set.world_aabb,
}
@ -344,7 +1012,12 @@ impl MeshLodGpu {
}
impl MeshGpu {
pub fn new(device: &wgpu::Device, mesh: &MeshModel, highlight: Highlight) -> Self {
pub fn new(
device: &wgpu::Device,
mesh: &MeshModel,
material: Option<&crate::scene::model::material_model::MeshMaterial>,
highlight: Highlight,
) -> Self {
let has_normals = mesh.normals.len() == mesh.verts.len();
// Blend the base colour toward the highlight so a selected / hovered
// solid reads clearly while keeping some shape shading.
@ -358,6 +1031,8 @@ impl MeshGpu {
}
None => mesh.color,
};
let (material_params, specular, ambient, advanced, flags) =
material_vertex_params(material);
let vertices: Vec<MeshVertex> = mesh
.verts
.iter()
@ -368,11 +1043,24 @@ impl MeshGpu {
} else {
[0.0, 1.0, 0.0]
};
let uv = material_uvs(material, pos, normal);
MeshVertex {
position: pos,
normal,
color,
position_low: mesh.verts_low.get(i).copied().unwrap_or([0.0; 3]),
material: material_params,
specular,
uv_diffuse: uv[0],
ambient,
advanced,
flags,
uv_specular: uv[1],
uv_reflection: uv[2],
uv_opacity: uv[3],
uv_bump: uv[4],
uv_refraction: uv[5],
uv_normal: uv[6],
}
})
.collect();

View file

@ -78,6 +78,8 @@ pub struct Pipeline {
/// writes, writes depth). Used in HiddenLine mode so 3D solids
/// occlude wires behind them without painting visible pixels.
mesh_depth_pipeline: wgpu::RenderPipeline,
mesh_material_bgl: wgpu::BindGroupLayout,
mesh_default_material_bind_group: wgpu::BindGroup,
face3d_pipeline: wgpu::RenderPipeline,
/// Depth-only variant of the face3d pipeline (no color writes,
/// writes depth). Paired with `mesh_depth_pipeline` for HiddenLine.
@ -839,9 +841,144 @@ impl Pipeline {
))),
});
let mesh_material_bgl =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mesh.material.bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 8,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 9,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 10,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 11,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 12,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 13,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 14,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let mesh_default_material_bind_group = mesh_gpu::create_material_bind_group(
device,
queue,
&mesh_material_bgl,
None,
);
let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh.pipeline_layout"),
bind_group_layouts: &[&frame_bgl],
bind_group_layouts: &[&frame_bgl, &mesh_material_bgl],
push_constant_ranges: &[],
});
@ -1437,6 +1574,8 @@ impl Pipeline {
mesh_wireframe_pipeline,
mesh_edge_black_pipeline,
mesh_depth_pipeline,
mesh_material_bgl,
mesh_default_material_bind_group,
face3d_pipeline,
face3d_depth_pipeline,
uniform_buffer,
@ -1757,6 +1896,18 @@ impl Pipeline {
(w.y - hy as f64) as f32,
(w.z - hz as f64) as f32,
],
material: [0.5, 0.0, 0.0, 0.0],
specular: [1.0, 1.0, 1.0, 1.0],
uv_diffuse: [0.0; 2],
ambient: [0.3, 0.3, 0.3, 0.0],
advanced: [1.0; 4],
flags: [0, 127, 0, 0],
uv_specular: [0.0; 2],
uv_reflection: [0.0; 2],
uv_opacity: [0.0; 2],
uv_bump: [0.0; 2],
uv_refraction: [0.0; 2],
uv_normal: [0.0; 2],
}
};
for g in &set.curved_gens {
@ -1863,6 +2014,28 @@ impl Pipeline {
}
}
}
if set.curved_gens.is_empty() || !set.complete {
let best = set.stored_silhouettes.iter().max_by(|left, right| {
let score = |silhouette: &crate::scene::model::mesh_model::StoredSilhouette| {
let direction = d3(silhouette.view_direction)
.normalize_or(glam::DVec3::NEG_Z);
direction.dot(view).abs()
};
score(left)
.partial_cmp(&score(right))
.unwrap_or(std::cmp::Ordering::Equal)
});
if let Some(silhouette) = best {
for (index, high) in silhouette.edge_verts.iter().copied().enumerate() {
let low = silhouette
.edge_verts_low
.get(index)
.copied()
.unwrap_or([0.0; 3]);
verts.push(mk(lo(high, low)));
}
}
}
}
if verts.is_empty() {
self.silhouette_vbuf = None;
@ -2020,8 +2193,21 @@ impl Pipeline {
/// set, drawn in a handful of calls). Selection/hover tint is intentionally
/// not applied here — the batch is geometry-only and stays resident across
/// camera moves and pick changes.
pub fn upload_mesh_batch(&mut self, device: &wgpu::Device, meshes: &[MeshLodSet]) {
let (chunks, _tris) = mesh_gpu::build_mesh_batch(device, meshes);
pub fn upload_mesh_batch(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
meshes: &[MeshLodSet],
) {
let (mut chunks, _tris) = mesh_gpu::build_mesh_batch(device, meshes);
for chunk in &mut chunks {
chunk.material_bind_group = Some(mesh_gpu::create_material_bind_group(
device,
queue,
&self.mesh_material_bgl,
chunk.material.as_ref(),
));
}
self.gpu_mesh_batch = chunks;
}
@ -2051,7 +2237,7 @@ impl Pipeline {
Some(h) if Some(h) == hover => Highlight::Hover,
_ => continue,
};
out.push(MeshGpu::new(device, m, mode));
out.push(MeshGpu::new(device, m, set.material.as_ref(), mode));
}
self.gpu_mesh_highlight = out;
}
@ -2377,6 +2563,13 @@ impl Pipeline {
// so both the opaque and the transparent tris write depth here.
pass.set_pipeline(&self.mesh_depth_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
if c.index_count != 0 {
pass.set_index_buffer(
@ -2395,6 +2588,13 @@ impl Pipeline {
}
pass.set_pipeline(&self.mesh_wireframe_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
// Plain-mesh triangulation edges.
if c.wire_index_count != 0 {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
@ -2419,6 +2619,13 @@ impl Pipeline {
if mesh_wireframe {
pass.set_pipeline(&self.mesh_wireframe_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.wire_index_count != 0 {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_index_buffer(
@ -2441,6 +2648,13 @@ impl Pipeline {
// Opaque fills first (they write depth).
pass.set_pipeline(&self.mesh_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.index_count == 0 {
continue;
}
@ -2453,6 +2667,13 @@ impl Pipeline {
// culling it via the depth buffer.
pass.set_pipeline(&self.mesh_transparent_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.transp_index_count == 0 {
continue;
}
@ -2469,6 +2690,7 @@ impl Pipeline {
// the tint shows on top even when the solid is behind others.
if !self.gpu_mesh_highlight.is_empty() {
pass.set_pipeline(&self.mesh_highlight_pipeline);
pass.set_bind_group(1, &self.mesh_default_material_bind_group, &[]);
for g in &self.gpu_mesh_highlight {
if g.index_count == 0 {
continue;
@ -2484,6 +2706,13 @@ impl Pipeline {
if want_solid_with_edges {
pass.set_pipeline(&self.mesh_edge_black_pipeline);
for c in &self.gpu_mesh_batch {
pass.set_bind_group(
1,
c.material_bind_group
.as_ref()
.unwrap_or(&self.mesh_default_material_bind_group),
&[],
);
if c.wire_index_count != 0 {
pass.set_vertex_buffer(0, c.vertex_buffer.slice(..));
pass.set_index_buffer(

View file

@ -215,6 +215,7 @@ pub fn toggle_invisible(entity: &mut EntityType) {
pub fn apply_color(entity: &mut EntityType, color: AcadColor) {
entity.as_entity_mut().set_color(color);
entity.common_mut().color_book_handle = None;
}
pub fn apply_line_weight(entity: &mut EntityType, lw: LineWeight) {

View file

@ -768,7 +768,7 @@ impl shader::Primitive for Primitive {
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.meshes))
{
inner.upload_mesh_batch(device, &vp.meshes[..]);
inner.upload_mesh_batch(device, queue, &vp.meshes[..]);
inner.cached_mesh_source = Some(Arc::clone(&vp.meshes));
inner.cached_mesh_batch_epoch = vp.geometry_epoch;
}
@ -1132,7 +1132,16 @@ pub(in crate::scene) fn render_style_for(
) -> ([f32; 4], f32, [f32; 8], f32, u8) {
let layer_name = &e.common().layer;
let (entity_color, aci) = {
let ec = &e.common().color;
let common = e.common();
let book_color = common
.color_book_handle
.filter(|handle| handle.is_valid())
.and_then(|handle| document.objects.get(&handle))
.and_then(|object| match object {
acadrust::objects::ObjectType::BookColor(book) => Some(&book.color),
_ => None,
});
let ec = book_color.unwrap_or(&common.color);
let resolved = if *ec == AcadColor::ByLayer {
document
.layers
@ -1176,6 +1185,14 @@ pub(in crate::scene) fn render_style_for(
(entity_color, pattern_length, pattern, line_weight_px, aci)
}
pub(crate) fn has_resolved_book_color(document: &CadDocument, e: &EntityType) -> bool {
e.common()
.color_book_handle
.filter(|handle| handle.is_valid())
.and_then(|handle| document.objects.get(&handle))
.is_some_and(|object| matches!(object, acadrust::objects::ObjectType::BookColor(_)))
}
/// Resolved render style used as the inheritance source for a block child's
/// ByBlock properties (the INSERT's own style) or its layer-0 properties (the
/// INSERT's *layer* style). Bundled so it threads through the block-expansion
@ -1254,9 +1271,10 @@ pub(crate) fn render_style_for_block_sub(
let common = e.common();
let on_l0 = is_effective_layer_zero(&common.layer);
let final_color = if common.color == AcadColor::ByBlock {
let has_book_color = has_resolved_book_color(document, e);
let final_color = if !has_book_color && common.color == AcadColor::ByBlock {
insert_color
} else if on_l0 && common.color == AcadColor::ByLayer {
} else if !has_book_color && on_l0 && common.color == AcadColor::ByLayer {
// Inherit the insert layer's RGB but keep the child's own transparency.
[l0.color[0], l0.color[1], l0.color[2], color[3]]
} else {