feat(scene): integrate standard DWG data
This commit is contained in:
parent
731df4a725
commit
eb23715605
38 changed files with 8365 additions and 607 deletions
|
|
@ -90,8 +90,8 @@ lyon_tessellation = "1.0.20"
|
|||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Track the verified DWG round-trip, I/O, and unified PERF fixes.
|
||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "de8d6b5" }
|
||||
# Track standard DWG entity data used by the scene integration.
|
||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "7aaea11" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Native enables the plugin host runtime (out-of-process plugins).
|
||||
|
|
|
|||
|
|
@ -1896,6 +1896,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
},
|
||||
|
|
@ -1986,6 +1988,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
|
|
@ -2078,6 +2082,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
|
|
@ -2097,6 +2103,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
|
|
@ -2131,6 +2139,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
|
|
@ -2150,6 +2160,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
|
|
@ -2247,6 +2259,8 @@ impl OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -671,6 +671,8 @@ impl super::OpenCADStudio {
|
|||
verts_low,
|
||||
normals,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,50 @@ use acadrust::{EntityType, Handle};
|
|||
/// shows a count-only summary instead. Bulk edits still go through the ribbon.
|
||||
const MAX_PROP_AGGREGATE: usize = 2_000;
|
||||
|
||||
fn material_color_text(color: &acadrust::objects::MaterialColor) -> String {
|
||||
let rgb = color
|
||||
.rgb
|
||||
.map(|value| format!("#{:06X}", value as u32 & 0x00ff_ffff))
|
||||
.unwrap_or_else(|| "Object".to_string());
|
||||
format!("{rgb}, factor {:.3}", color.factor)
|
||||
}
|
||||
|
||||
fn material_map_text(map: &acadrust::objects::MaterialMap) -> String {
|
||||
let source = match map.source {
|
||||
1 => {
|
||||
if map.file_name.trim().is_empty() {
|
||||
"File".to_string()
|
||||
} else {
|
||||
map.file_name.clone()
|
||||
}
|
||||
}
|
||||
2 => format!(
|
||||
"Procedural {}",
|
||||
map.texture.as_ref().map_or(0, |texture| texture.mode)
|
||||
),
|
||||
_ => "None".to_string(),
|
||||
};
|
||||
format!(
|
||||
"{source}; blend {:.3}; projection {}; tiling {}; auto {}",
|
||||
map.blend_factor, map.projection, map.tiling, map.auto_transform
|
||||
)
|
||||
}
|
||||
|
||||
fn visual_style_properties_text(style: &acadrust::objects::VisualStyle) -> String {
|
||||
style
|
||||
.properties
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, property)| {
|
||||
format!(
|
||||
"{}: {:?} (enabled {})",
|
||||
index, property.value, property.enabled
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Rebuild the PropertiesPanel from the current entity selection.
|
||||
/// Preserves UI state (open pickers, edit buffer) across refreshes.
|
||||
|
|
@ -93,7 +137,12 @@ impl OpenCADStudio {
|
|||
let mut panel = match selected.len() {
|
||||
0 => ui::PropertiesPanel::empty(),
|
||||
1 => {
|
||||
let (handle, entity) = selected[0];
|
||||
let (handle, source_entity) = selected[0];
|
||||
let contextual = crate::scene::annotative::entity_for_active_context(
|
||||
&self.tabs[i].scene.document,
|
||||
source_entity,
|
||||
);
|
||||
let entity = contextual.as_ref();
|
||||
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
|
||||
let mut sections =
|
||||
dispatch::properties_sectioned(handle, entity, &text_style_names);
|
||||
|
|
@ -140,6 +189,329 @@ impl OpenCADStudio {
|
|||
};
|
||||
}
|
||||
}
|
||||
let effective_handle = match common.material_flags {
|
||||
3 => common.material_handle,
|
||||
0 => doc
|
||||
.layers
|
||||
.get(&common.layer)
|
||||
.map(|layer| layer.material)
|
||||
.filter(|handle| handle.is_valid()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(acadrust::objects::ObjectType::Material(material)) =
|
||||
effective_handle.and_then(|handle| doc.objects.get(&handle))
|
||||
{
|
||||
use crate::entities::common::ro_prop;
|
||||
sections.push(crate::scene::model::object::PropSection {
|
||||
title: "Material Details".to_string(),
|
||||
props: vec![
|
||||
ro_prop("Name", "mat_name", material.name.clone()),
|
||||
ro_prop(
|
||||
"Description",
|
||||
"mat_description",
|
||||
material.description.clone(),
|
||||
),
|
||||
ro_prop(
|
||||
"Ambient",
|
||||
"mat_ambient",
|
||||
material_color_text(&material.ambient_color),
|
||||
),
|
||||
ro_prop(
|
||||
"Diffuse",
|
||||
"mat_diffuse",
|
||||
material_color_text(&material.diffuse_color),
|
||||
),
|
||||
ro_prop(
|
||||
"Specular",
|
||||
"mat_specular",
|
||||
material_color_text(&material.specular_color),
|
||||
),
|
||||
ro_prop(
|
||||
"Gloss",
|
||||
"mat_gloss",
|
||||
format!("{:.3}", material.specular_gloss_factor),
|
||||
),
|
||||
ro_prop(
|
||||
"Opacity",
|
||||
"mat_opacity",
|
||||
format!("{:.3}", material.opacity_percent),
|
||||
),
|
||||
ro_prop(
|
||||
"Reflectivity",
|
||||
"mat_reflectivity",
|
||||
format!("{:.3}", material.reflectivity),
|
||||
),
|
||||
ro_prop(
|
||||
"Translucence",
|
||||
"mat_translucence",
|
||||
format!("{:.3}", material.translucence),
|
||||
),
|
||||
ro_prop(
|
||||
"Refraction",
|
||||
"mat_refraction",
|
||||
format!("{:.3}", material.refraction_index),
|
||||
),
|
||||
ro_prop(
|
||||
"Self Illumination",
|
||||
"mat_self_illumination",
|
||||
format!("{:.3}", material.self_illumination),
|
||||
),
|
||||
ro_prop(
|
||||
"Luminance",
|
||||
"mat_luminance",
|
||||
format!(
|
||||
"{:.3} (mode {})",
|
||||
material.luminance, material.luminance_mode
|
||||
),
|
||||
),
|
||||
ro_prop(
|
||||
"Diffuse Map",
|
||||
"mat_diffuse_map",
|
||||
material_map_text(&material.diffuse_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Specular Map",
|
||||
"mat_specular_map",
|
||||
material_map_text(&material.specular_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Reflection Map",
|
||||
"mat_reflection_map",
|
||||
material_map_text(&material.reflection_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Opacity Map",
|
||||
"mat_opacity_map",
|
||||
material_map_text(&material.opacity_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Bump Map",
|
||||
"mat_bump_map",
|
||||
material_map_text(&material.bump_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Refraction Map",
|
||||
"mat_refraction_map",
|
||||
material_map_text(&material.refraction_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Normal Map",
|
||||
"mat_normal_map",
|
||||
material_map_text(&material.normal_map),
|
||||
),
|
||||
ro_prop(
|
||||
"Render Flags",
|
||||
"mat_render_flags",
|
||||
format!(
|
||||
"illumination {}; channels {}; mode {}; two-sided {}",
|
||||
material.illumination_model,
|
||||
material.channel_flags,
|
||||
material.mode,
|
||||
material.two_sided_material
|
||||
),
|
||||
),
|
||||
ro_prop(
|
||||
"Advanced",
|
||||
"mat_advanced",
|
||||
format!(
|
||||
"normal {:.3}; bump {:.3}; reflect {:.3}; transmit {:.3}",
|
||||
material.normal_map_strength,
|
||||
material.indirect_bump_scale,
|
||||
material.reflectance_scale,
|
||||
material.transmittance_scale
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let common = entity.common();
|
||||
if let Some(acadrust::objects::ObjectType::BookColor(book)) = common
|
||||
.color_book_handle
|
||||
.filter(|handle| handle.is_valid())
|
||||
.and_then(|handle| doc.objects.get(&handle))
|
||||
{
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) =
|
||||
section.props.iter_mut().find(|row| row.field == "color")
|
||||
{
|
||||
row.value =
|
||||
crate::scene::model::object::PropValue::ColorChoice(
|
||||
book.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
use crate::entities::common::ro_prop;
|
||||
sections.push(crate::scene::model::object::PropSection {
|
||||
title: "Color Book".to_string(),
|
||||
props: vec![
|
||||
ro_prop("Book", "book_color_book", book.book_name.clone()),
|
||||
ro_prop(
|
||||
"Color Name",
|
||||
"book_color_name",
|
||||
book.color_name.clone(),
|
||||
),
|
||||
ro_prop(
|
||||
"Color",
|
||||
"book_color_value",
|
||||
format!("{:?}", book.color),
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
let style_handles = [
|
||||
("Full", common.full_visual_style_handle),
|
||||
("Face", common.face_visual_style_handle),
|
||||
("Edge", common.edge_visual_style_handle),
|
||||
];
|
||||
for (scope, handle) in style_handles {
|
||||
let Some((handle, style)) = handle
|
||||
.filter(|handle| handle.is_valid())
|
||||
.and_then(|handle| {
|
||||
doc.objects.get(&handle).and_then(|object| match object {
|
||||
acadrust::objects::ObjectType::VisualStyle(style) => {
|
||||
Some((handle, style))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
use crate::entities::common::ro_prop;
|
||||
sections.push(crate::scene::model::object::PropSection {
|
||||
title: format!("{scope} Visual Style"),
|
||||
props: vec![
|
||||
ro_prop(
|
||||
"Handle",
|
||||
"vs_handle",
|
||||
format!("{:X}", handle.value()),
|
||||
),
|
||||
ro_prop(
|
||||
"Description",
|
||||
"vs_description",
|
||||
style.description.clone(),
|
||||
),
|
||||
ro_prop(
|
||||
"Type",
|
||||
"vs_type",
|
||||
style.style_type.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Face",
|
||||
"vs_face",
|
||||
format!(
|
||||
"lighting {}; quality {}; color {}; modifiers {}",
|
||||
style.face_lighting_model,
|
||||
style.face_lighting_quality,
|
||||
style.face_color_mode,
|
||||
style.face_modifier
|
||||
),
|
||||
),
|
||||
ro_prop(
|
||||
"Edges",
|
||||
"vs_edges",
|
||||
format!(
|
||||
"model {}; style {}",
|
||||
style.edge_model, style.edge_style
|
||||
),
|
||||
),
|
||||
ro_prop(
|
||||
"Extended Lighting",
|
||||
"vs_extended_lighting",
|
||||
style.extended_lighting_model.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Internal",
|
||||
"vs_internal",
|
||||
style.internal_use_only.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Property Bag",
|
||||
"vs_properties",
|
||||
visual_style_properties_text(style),
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
entity,
|
||||
acadrust::EntityType::Solid3D(_)
|
||||
| acadrust::EntityType::Region(_)
|
||||
| acadrust::EntityType::Body(_)
|
||||
| acadrust::EntityType::Surface(_)
|
||||
| acadrust::EntityType::Mesh(_)
|
||||
| acadrust::EntityType::PolygonMesh(_)
|
||||
| acadrust::EntityType::PolyfaceMesh(_)
|
||||
) {
|
||||
if let Some(mesh) = self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.get(&handle)
|
||||
.or_else(|| self.tabs[i].scene.block_meshes.get(&handle))
|
||||
{
|
||||
use crate::entities::common::ro_prop;
|
||||
let metrics = mesh.metrics;
|
||||
let mut props = vec![
|
||||
ro_prop(
|
||||
"Vertices",
|
||||
"mesh_vertices",
|
||||
metrics.vertices.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Triangles",
|
||||
"mesh_triangles",
|
||||
metrics.triangles.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Surface Area",
|
||||
"mesh_surface_area",
|
||||
format!("{:.6}", metrics.surface_area),
|
||||
),
|
||||
ro_prop(
|
||||
"Centroid",
|
||||
"mesh_centroid",
|
||||
format!(
|
||||
"{:.6}, {:.6}, {:.6}",
|
||||
metrics.centroid[0],
|
||||
metrics.centroid[1],
|
||||
metrics.centroid[2]
|
||||
),
|
||||
),
|
||||
ro_prop(
|
||||
"Tessellation",
|
||||
"mesh_complete",
|
||||
if mesh.complete { "Complete" } else { "Partial" },
|
||||
),
|
||||
];
|
||||
if mesh.complete
|
||||
&& matches!(
|
||||
entity,
|
||||
acadrust::EntityType::Solid3D(_)
|
||||
| acadrust::EntityType::Region(_)
|
||||
| acadrust::EntityType::Body(_)
|
||||
)
|
||||
{
|
||||
props.insert(
|
||||
3,
|
||||
ro_prop(
|
||||
"Volume",
|
||||
"mesh_volume",
|
||||
format!("{:.6}", metrics.volume),
|
||||
),
|
||||
);
|
||||
}
|
||||
sections.push(crate::scene::model::object::PropSection {
|
||||
title: "Mass Properties".to_string(),
|
||||
props,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Uniform-scale checkbox for block references (#427):
|
||||
|
|
@ -1090,7 +1462,11 @@ impl OpenCADStudio {
|
|||
let selected = self.tabs[i].scene.selected_entities();
|
||||
if selected.len() == 1 {
|
||||
let (handle, entity) = selected[0];
|
||||
let grips = dispatch::grips(entity)
|
||||
let contextual = crate::scene::annotative::entity_for_active_context(
|
||||
&self.tabs[i].scene.document,
|
||||
entity,
|
||||
);
|
||||
let grips = dispatch::grips(contextual.as_ref())
|
||||
.into_iter()
|
||||
.map(|mut g| {
|
||||
// Subtract in f64: at UTM magnitudes an f32 cast before
|
||||
|
|
@ -1124,12 +1500,21 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
|
||||
let mut context_object_changed = false;
|
||||
for &handle in handles {
|
||||
context_object_changed |=
|
||||
crate::scene::annotative::sync_active_context_from_entity(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
);
|
||||
// Hatch / SOLID fills render from prebuilt cached models; rebuild
|
||||
// them or pattern edits (scale, background, …) stay invisible
|
||||
// (#415).
|
||||
self.tabs[i].scene.refresh_fill_model(handle);
|
||||
}
|
||||
if context_object_changed {
|
||||
self.tabs[i].scene.poison_undo_recording();
|
||||
}
|
||||
// Solid (ACIS) meshes bake their colour into the mesh, so a colour /
|
||||
// layer change needs an explicit recolour — re-tessellating wires
|
||||
// alone wouldn't update them.
|
||||
|
|
|
|||
|
|
@ -491,6 +491,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
};
|
||||
|
||||
self.tabs[i].current_path = Some(path.clone());
|
||||
self.tabs[i].scene.material_base_dir =
|
||||
path.parent().map(std::path::Path::to_path_buf);
|
||||
self.tabs[i].scene.document = doc;
|
||||
// A file saved without the built-in Standard styles (foreign
|
||||
// or damaged) gets them re-seeded so nothing dangles (#366).
|
||||
|
|
|
|||
|
|
@ -1967,6 +1967,7 @@ impl OpenCADStudio {
|
|||
self.scale_popup_open = false;
|
||||
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||
tab.scene.annotation_scale = scale;
|
||||
util::sync_annotation_scale_header(&mut tab.scene);
|
||||
tab.scene.bump_geometry();
|
||||
}
|
||||
Task::none()
|
||||
|
|
|
|||
|
|
@ -1224,6 +1224,102 @@ use crate::scene::convert::tessellate::{
|
|||
};
|
||||
use crate::scene::model::wire_model::{SnapHint, WireModel};
|
||||
|
||||
fn apply_dimension_breaks(
|
||||
document: &CadDocument,
|
||||
dimension: Handle,
|
||||
lines: &mut Vec<[f32; 3]>,
|
||||
) {
|
||||
let references: Vec<_> = document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|object| {
|
||||
let acadrust::objects::ObjectType::DataObject(object) = object else {
|
||||
return None;
|
||||
};
|
||||
let acadrust::objects::DataObjectData::BreakData(data) = &object.data
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
(data.dimension_reference == dimension).then_some(&data.point_references)
|
||||
})
|
||||
.flatten()
|
||||
.filter(|reference| {
|
||||
let points = [reference.first_point, reference.second_point];
|
||||
points.iter().all(|point| {
|
||||
point.x.is_finite() && point.y.is_finite() && point.z.is_finite()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if references.is_empty() || lines.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut output = Vec::with_capacity(lines.len() + references.len() * 3);
|
||||
for run in lines.split(|point| point[0].is_nan()) {
|
||||
for segment in run.windows(2) {
|
||||
let a = Vec3::from_array(segment[0]);
|
||||
let b = Vec3::from_array(segment[1]);
|
||||
let direction = b - a;
|
||||
let length_squared = direction.length_squared();
|
||||
if length_squared <= 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let mut intervals = vec![(0.0f32, 1.0f32)];
|
||||
for reference in &references {
|
||||
let first = vec3_local(reference.first_point);
|
||||
let second = vec3_local(reference.second_point);
|
||||
let t1 = (first - a).dot(direction) / length_squared;
|
||||
let t2 = (second - a).dot(direction) / length_squared;
|
||||
let closest1 = a + direction * t1.clamp(0.0, 1.0);
|
||||
let closest2 = a + direction * t2.clamp(0.0, 1.0);
|
||||
let requested_gap = (second - first).length();
|
||||
let tolerance = (requested_gap * 0.25)
|
||||
.max(direction.length() * 1e-5)
|
||||
.max(1e-4);
|
||||
if (first - closest1).length() > tolerance
|
||||
|| (second - closest2).length() > tolerance
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let half_point_gap = if requested_gap <= 1e-6 {
|
||||
(tolerance / direction.length()).min(0.1)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let cut_start = (t1.min(t2) - half_point_gap).clamp(0.0, 1.0);
|
||||
let cut_end = (t1.max(t2) + half_point_gap).clamp(0.0, 1.0);
|
||||
if cut_end <= cut_start {
|
||||
continue;
|
||||
}
|
||||
let mut remaining = Vec::new();
|
||||
for (start, end) in intervals {
|
||||
if cut_end <= start || cut_start >= end {
|
||||
remaining.push((start, end));
|
||||
continue;
|
||||
}
|
||||
if cut_start > start {
|
||||
remaining.push((start, cut_start));
|
||||
}
|
||||
if cut_end < end {
|
||||
remaining.push((cut_end, end));
|
||||
}
|
||||
}
|
||||
intervals = remaining;
|
||||
}
|
||||
for (start, end) in intervals {
|
||||
if end - start > 1e-6 {
|
||||
add_segment(
|
||||
&mut output,
|
||||
a + direction * start,
|
||||
a + direction * end,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*lines = output;
|
||||
}
|
||||
|
||||
pub trait DimensionTess {
|
||||
fn tessellate(
|
||||
&self,
|
||||
|
|
@ -1464,6 +1560,7 @@ fn tessellate_dimension_inner(
|
|||
// supersedes it. Read but not honoured.
|
||||
let _ = s.dimunit;
|
||||
}
|
||||
apply_dimension_breaks(document, handle, &mut geom.dim_lines);
|
||||
// Dimension entity fields that the render path doesn't yet use but are
|
||||
// preserved on save:
|
||||
// - base.insertion_point: legacy anchor reference; render uses
|
||||
|
|
|
|||
1609
src/entities/extended.rs
Normal file
1609
src/entities/extended.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -53,6 +53,14 @@ pub fn resolve(document: &CadDocument, host: Handle) -> Option<String> {
|
|||
acadrust::fields::resolve(document, host, &OcsFieldContext)
|
||||
}
|
||||
|
||||
pub fn resolve_handle(
|
||||
document: &CadDocument,
|
||||
field: Handle,
|
||||
host: Handle,
|
||||
) -> Option<String> {
|
||||
acadrust::fields::resolve_handle(document, field, host, &OcsFieldContext)
|
||||
}
|
||||
|
||||
/// Seconds since the Unix epoch. Native uses the system clock; wasm uses the JS
|
||||
/// `Date` clock. This platform-specific read stays in the app, not the library.
|
||||
fn epoch_secs() -> i64 {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, Polyface
|
|||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{ro_prop as ro, square_grip};
|
||||
use crate::entities::common::{parse_f64, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
|
||||
/// Triangulate a planar (possibly concave) polygon into a flat triangle-soup
|
||||
|
|
@ -319,63 +319,13 @@ impl TruckConvertible for PolygonMesh {
|
|||
return None;
|
||||
}
|
||||
|
||||
let closed_m = self
|
||||
.flags
|
||||
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_M);
|
||||
let closed_n = self
|
||||
.flags
|
||||
.contains(acadrust::entities::PolygonMeshFlags::CLOSED_N);
|
||||
|
||||
let pt = |i: usize, j: usize| -> [f64; 3] {
|
||||
let v = &self.vertices[i * n + j];
|
||||
[v.location.x, v.location.y, v.location.z]
|
||||
};
|
||||
|
||||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
||||
|
||||
// Rows (M direction).
|
||||
for i in 0..m {
|
||||
pts.push([f64::NAN; 3]);
|
||||
for j in 0..n {
|
||||
pts.push(pt(i, j));
|
||||
}
|
||||
if closed_n {
|
||||
pts.push(pt(i, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Columns (N direction).
|
||||
for j in 0..n {
|
||||
pts.push([f64::NAN; 3]);
|
||||
for i in 0..m {
|
||||
pts.push(pt(i, j));
|
||||
}
|
||||
if closed_m {
|
||||
pts.push(pt(0, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Fill: triangulate each grid quad (two triangles per cell).
|
||||
let mi = if closed_m { m } else { m - 1 };
|
||||
let ni = if closed_n { n } else { n - 1 };
|
||||
for i in 0..mi {
|
||||
for j in 0..ni {
|
||||
let p00 = pt(i, j);
|
||||
let p10 = pt((i + 1) % m, j);
|
||||
let p01 = pt(i, (j + 1) % n);
|
||||
let p11 = pt((i + 1) % m, (j + 1) % n);
|
||||
fill_tris.extend_from_slice(&[p00, p10, p11, p00, p11, p01]);
|
||||
}
|
||||
}
|
||||
|
||||
Some(TruckEntity {
|
||||
pick_tris: Vec::new(),
|
||||
object: TruckObject::Lines(pts),
|
||||
object: TruckObject::Lines(Vec::new()),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -491,47 +441,13 @@ impl TruckConvertible for PolyfaceMesh {
|
|||
return None;
|
||||
}
|
||||
|
||||
let get_v = |idx: i16| -> Option<[f64; 3]> {
|
||||
let i = (idx.abs() as usize).checked_sub(1)?;
|
||||
let v = self.vertices.get(i)?;
|
||||
Some([v.location.x, v.location.y, v.location.z])
|
||||
};
|
||||
|
||||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
||||
|
||||
for face in &self.faces {
|
||||
// Indices: 0 means unused. Negative = invisible edge (still render for wireframe).
|
||||
let indices = [face.index1, face.index2, face.index3, face.index4];
|
||||
let verts: Vec<[f64; 3]> = indices
|
||||
.iter()
|
||||
.filter(|&&i| i != 0)
|
||||
.filter_map(|&i| get_v(i))
|
||||
.collect();
|
||||
|
||||
if verts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
pts.push([f64::NAN; 3]);
|
||||
for &p in &verts {
|
||||
pts.push(p);
|
||||
}
|
||||
// Close the face polygon.
|
||||
pts.push(verts[0]);
|
||||
|
||||
// Ear-clip the face for solid fill (handles concave faces).
|
||||
if verts.len() >= 3 {
|
||||
fill_tris.extend(triangulate_planar(&verts));
|
||||
}
|
||||
}
|
||||
|
||||
Some(TruckEntity {
|
||||
pick_tris: Vec::new(),
|
||||
object: TruckObject::Lines(pts),
|
||||
object: TruckObject::Lines(Vec::new()),
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
fill_tris,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -637,53 +553,527 @@ impl Transformable for PolyfaceMesh {
|
|||
// ── Mesh (SubD mesh) ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Modern subdivision mesh — distinct from PolygonMesh. The render path emits
|
||||
// the file's per-edge wireframe and triangulates each face into fill_tris so
|
||||
// solid views still draw a shaded surface. Subdivision-level smoothing is
|
||||
// honoured only as metadata; we don't run a Catmull-Clark refinement pass
|
||||
// here yet.
|
||||
// the refined per-edge wireframe and triangulates each face into fill_tris so
|
||||
// solid views draw the same Catmull-Clark surface described by the DWG.
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RefinedMesh {
|
||||
vertices: Vec<[f64; 3]>,
|
||||
faces: Vec<Vec<usize>>,
|
||||
creases: std::collections::HashMap<(usize, usize), f64>,
|
||||
}
|
||||
|
||||
fn edge_key(a: usize, b: usize) -> (usize, usize) {
|
||||
if a < b { (a, b) } else { (b, a) }
|
||||
}
|
||||
|
||||
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||||
}
|
||||
|
||||
fn mul3(a: [f64; 3], scale: f64) -> [f64; 3] {
|
||||
[a[0] * scale, a[1] * scale, a[2] * scale]
|
||||
}
|
||||
|
||||
fn mix3(a: [f64; 3], b: [f64; 3], amount: f64) -> [f64; 3] {
|
||||
add3(mul3(a, 1.0 - amount), mul3(b, amount))
|
||||
}
|
||||
|
||||
fn mean_points<'a>(points: impl Iterator<Item = &'a [f64; 3]>) -> [f64; 3] {
|
||||
let mut sum = [0.0; 3];
|
||||
let mut count = 0usize;
|
||||
for point in points {
|
||||
sum = add3(sum, *point);
|
||||
count += 1;
|
||||
}
|
||||
if count == 0 {
|
||||
sum
|
||||
} else {
|
||||
mul3(sum, 1.0 / count as f64)
|
||||
}
|
||||
}
|
||||
|
||||
fn base_refined_mesh(mesh: &Mesh) -> RefinedMesh {
|
||||
let vertices: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| [v.x, v.y, v.z]).collect();
|
||||
let faces = mesh
|
||||
.faces
|
||||
.iter()
|
||||
.filter_map(|face| {
|
||||
let valid: Vec<usize> = face
|
||||
.vertices
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&index| index < vertices.len())
|
||||
.collect();
|
||||
(valid.len() >= 3).then_some(valid)
|
||||
})
|
||||
.collect();
|
||||
let creases = mesh
|
||||
.edges
|
||||
.iter()
|
||||
.filter_map(|edge| {
|
||||
let sharpness = edge.crease?;
|
||||
(edge.start < vertices.len() && edge.end < vertices.len() && sharpness > 0.0)
|
||||
.then_some((edge_key(edge.start, edge.end), sharpness))
|
||||
})
|
||||
.collect();
|
||||
RefinedMesh {
|
||||
vertices,
|
||||
faces,
|
||||
creases,
|
||||
}
|
||||
}
|
||||
|
||||
fn subdivide_catmull_clark(mesh: &RefinedMesh, blend_crease: bool) -> RefinedMesh {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
if mesh.faces.is_empty() {
|
||||
return mesh.clone();
|
||||
}
|
||||
|
||||
let face_points: Vec<[f64; 3]> = mesh
|
||||
.faces
|
||||
.iter()
|
||||
.map(|face| mean_points(face.iter().filter_map(|&index| mesh.vertices.get(index))))
|
||||
.collect();
|
||||
|
||||
let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
|
||||
let mut vertex_faces: Vec<Vec<usize>> = vec![Vec::new(); mesh.vertices.len()];
|
||||
let mut vertex_edges: Vec<HashSet<(usize, usize)>> = vec![HashSet::new(); mesh.vertices.len()];
|
||||
for (face_index, face) in mesh.faces.iter().enumerate() {
|
||||
for &vertex in face {
|
||||
if let Some(faces) = vertex_faces.get_mut(vertex) {
|
||||
faces.push(face_index);
|
||||
}
|
||||
}
|
||||
for corner in 0..face.len() {
|
||||
let a = face[corner];
|
||||
let b = face[(corner + 1) % face.len()];
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
let key = edge_key(a, b);
|
||||
edge_faces.entry(key).or_default().push(face_index);
|
||||
if let Some(edges) = vertex_edges.get_mut(a) {
|
||||
edges.insert(key);
|
||||
}
|
||||
if let Some(edges) = vertex_edges.get_mut(b) {
|
||||
edges.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut vertex_points = Vec::with_capacity(mesh.vertices.len());
|
||||
for (vertex_index, &point) in mesh.vertices.iter().enumerate() {
|
||||
let incident_edges = &vertex_edges[vertex_index];
|
||||
let incident_faces = &vertex_faces[vertex_index];
|
||||
if incident_edges.is_empty() || incident_faces.is_empty() {
|
||||
vertex_points.push(point);
|
||||
continue;
|
||||
}
|
||||
|
||||
let face_average =
|
||||
mean_points(incident_faces.iter().filter_map(|&index| face_points.get(index)));
|
||||
let edge_midpoints: Vec<[f64; 3]> = incident_edges
|
||||
.iter()
|
||||
.filter_map(|&(a, b)| {
|
||||
let other = if a == vertex_index { b } else { a };
|
||||
mesh.vertices
|
||||
.get(other)
|
||||
.map(|&p| mul3(add3(point, p), 0.5))
|
||||
})
|
||||
.collect();
|
||||
let edge_average = mean_points(edge_midpoints.iter());
|
||||
let n = incident_faces.len() as f64;
|
||||
let smooth = mul3(
|
||||
add3(add3(face_average, mul3(edge_average, 2.0)), mul3(point, n - 3.0)),
|
||||
1.0 / n,
|
||||
);
|
||||
|
||||
let mut sharp_neighbours: Vec<(f64, [f64; 3])> = incident_edges
|
||||
.iter()
|
||||
.filter_map(|&key| {
|
||||
let boundary = edge_faces.get(&key).is_some_and(|faces| faces.len() == 1);
|
||||
let sharpness = if boundary {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
mesh.creases.get(&key).copied().unwrap_or(0.0)
|
||||
};
|
||||
if sharpness <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let other = if key.0 == vertex_index { key.1 } else { key.0 };
|
||||
mesh.vertices.get(other).copied().map(|p| (sharpness, p))
|
||||
})
|
||||
.collect();
|
||||
sharp_neighbours.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
|
||||
let crease_point = match sharp_neighbours.as_slice() {
|
||||
[(_, first), (_, second), ..] => mul3(
|
||||
add3(add3(mul3(point, 6.0), *first), *second),
|
||||
1.0 / 8.0,
|
||||
),
|
||||
_ => smooth,
|
||||
};
|
||||
let corner_point = if sharp_neighbours.len() >= 3 {
|
||||
point
|
||||
} else {
|
||||
crease_point
|
||||
};
|
||||
let sharpness = sharp_neighbours
|
||||
.get(1)
|
||||
.map(|item| item.0)
|
||||
.unwrap_or(0.0);
|
||||
let amount = if blend_crease {
|
||||
sharpness.clamp(0.0, 1.0)
|
||||
} else if sharpness > 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
vertex_points.push(mix3(smooth, corner_point, amount));
|
||||
}
|
||||
|
||||
let mut edge_point_indices = HashMap::with_capacity(edge_faces.len());
|
||||
let mut vertices = vertex_points;
|
||||
for (&key, adjacent_faces) in &edge_faces {
|
||||
let Some((&a, &b)) = mesh.vertices.get(key.0).zip(mesh.vertices.get(key.1)) else {
|
||||
continue;
|
||||
};
|
||||
let midpoint = mul3(add3(a, b), 0.5);
|
||||
let smooth = if adjacent_faces.len() == 2 {
|
||||
let f0 = face_points[adjacent_faces[0]];
|
||||
let f1 = face_points[adjacent_faces[1]];
|
||||
mul3(add3(add3(a, b), add3(f0, f1)), 0.25)
|
||||
} else {
|
||||
midpoint
|
||||
};
|
||||
let boundary = adjacent_faces.len() != 2;
|
||||
let sharpness = mesh.creases.get(&key).copied().unwrap_or(0.0);
|
||||
let amount = if boundary {
|
||||
1.0
|
||||
} else if blend_crease {
|
||||
sharpness.clamp(0.0, 1.0)
|
||||
} else if sharpness > 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
edge_point_indices.insert(key, vertices.len());
|
||||
vertices.push(mix3(smooth, midpoint, amount));
|
||||
}
|
||||
|
||||
let face_point_start = vertices.len();
|
||||
vertices.extend(face_points.iter().copied());
|
||||
let mut faces = Vec::new();
|
||||
let mut creases = HashMap::new();
|
||||
for (face_index, face) in mesh.faces.iter().enumerate() {
|
||||
for corner in 0..face.len() {
|
||||
let vertex = face[corner];
|
||||
let next_key = edge_key(vertex, face[(corner + 1) % face.len()]);
|
||||
let prev_key = edge_key(face[(corner + face.len() - 1) % face.len()], vertex);
|
||||
let (Some(&next_edge), Some(&prev_edge)) = (
|
||||
edge_point_indices.get(&next_key),
|
||||
edge_point_indices.get(&prev_key),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
faces.push(vec![
|
||||
vertex,
|
||||
next_edge,
|
||||
face_point_start + face_index,
|
||||
prev_edge,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
for (&key, &sharpness) in &mesh.creases {
|
||||
let Some(&edge_point) = edge_point_indices.get(&key) else {
|
||||
continue;
|
||||
};
|
||||
let child_sharpness = (sharpness - 1.0).max(0.0);
|
||||
if child_sharpness > 0.0 {
|
||||
creases.insert(edge_key(key.0, edge_point), child_sharpness);
|
||||
creases.insert(edge_key(edge_point, key.1), child_sharpness);
|
||||
}
|
||||
}
|
||||
|
||||
RefinedMesh {
|
||||
vertices,
|
||||
faces,
|
||||
creases,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_mesh(mesh: &Mesh) -> RefinedMesh {
|
||||
let mut refined = base_refined_mesh(mesh);
|
||||
for _ in 0..mesh.subdivision_level.clamp(0, 4) {
|
||||
refined = subdivide_catmull_clark(&refined, mesh.blend_crease);
|
||||
}
|
||||
refined
|
||||
}
|
||||
|
||||
fn face_triangle_indices(
|
||||
vertices: &[[f64; 3]],
|
||||
faces: &[Vec<usize>],
|
||||
) -> (Vec<u32>, Vec<usize>) {
|
||||
let mut indices = Vec::new();
|
||||
let mut triangle_faces = Vec::new();
|
||||
for (face_index, face) in faces.iter().enumerate() {
|
||||
let polygon: Vec<[f64; 3]> = face
|
||||
.iter()
|
||||
.filter_map(|&index| vertices.get(index).copied())
|
||||
.collect();
|
||||
if polygon.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
for triangle in triangulate_planar(&polygon).chunks_exact(3) {
|
||||
let mut mapped = [0u32; 3];
|
||||
let mut valid = true;
|
||||
for corner in 0..3 {
|
||||
let Some(local) = polygon.iter().position(|point| *point == triangle[corner]) else {
|
||||
valid = false;
|
||||
break;
|
||||
};
|
||||
mapped[corner] = face[local] as u32;
|
||||
}
|
||||
if valid && mapped[0] != mapped[1] && mapped[1] != mapped[2] && mapped[2] != mapped[0] {
|
||||
indices.extend(mapped);
|
||||
triangle_faces.push(face_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
(indices, triangle_faces)
|
||||
}
|
||||
|
||||
fn vertex_normals(vertices: &[[f64; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
|
||||
let mut normals = vec![[0.0f64; 3]; vertices.len()];
|
||||
for triangle in indices.chunks_exact(3) {
|
||||
let (Some(&a), Some(&b), Some(&c)) = (
|
||||
vertices.get(triangle[0] as usize),
|
||||
vertices.get(triangle[1] as usize),
|
||||
vertices.get(triangle[2] as usize),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
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 normal = [
|
||||
ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0],
|
||||
];
|
||||
for &index in triangle {
|
||||
if let Some(sum) = normals.get_mut(index as usize) {
|
||||
*sum = add3(*sum, normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
normals
|
||||
.into_iter()
|
||||
.map(|normal| {
|
||||
let length =
|
||||
(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
|
||||
if length > 1e-15 {
|
||||
[
|
||||
(normal[0] / length) as f32,
|
||||
(normal[1] / length) as f32,
|
||||
(normal[2] / length) as f32,
|
||||
]
|
||||
} else {
|
||||
[0.0, 0.0, 1.0]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn append_feature_edge(
|
||||
edge_verts: &mut Vec<[f32; 3]>,
|
||||
edge_verts_low: &mut Vec<[f32; 3]>,
|
||||
vertices: &[[f64; 3]],
|
||||
a: usize,
|
||||
b: usize,
|
||||
) {
|
||||
for index in [a, b] {
|
||||
let Some(&[x, y, z]) = vertices.get(index) else {
|
||||
continue;
|
||||
};
|
||||
let high = [x as f32, y as f32, z as f32];
|
||||
edge_verts.push(high);
|
||||
edge_verts_low.push([
|
||||
(x - high[0] as f64) as f32,
|
||||
(y - high[1] as f64) as f32,
|
||||
(z - high[2] as f64) as f32,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
fn make_mesh_lod_set(
|
||||
name: String,
|
||||
color: [f32; 4],
|
||||
vertices: Vec<[f64; 3]>,
|
||||
faces: Vec<Vec<usize>>,
|
||||
visible_edges: std::collections::HashSet<(usize, usize)>,
|
||||
face_colors: &[Option<[f32; 4]>],
|
||||
) -> Option<crate::scene::model::mesh_model::MeshLodSet> {
|
||||
let (indices, triangle_faces) = face_triangle_indices(&vertices, &faces);
|
||||
if indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let normals = vertex_normals(&vertices, &indices);
|
||||
let triangle_colors = triangle_faces
|
||||
.iter()
|
||||
.map(|&face| face_colors.get(face).copied().flatten())
|
||||
.collect();
|
||||
let model = crate::scene::convert::solid3d_tess::finalize_mesh(
|
||||
name,
|
||||
vertices.clone(),
|
||||
normals,
|
||||
indices,
|
||||
Vec::new(),
|
||||
triangle_colors,
|
||||
color,
|
||||
None,
|
||||
);
|
||||
let mut set = crate::scene::model::mesh_model::MeshLodSet::from_lods(vec![model]);
|
||||
for (a, b) in visible_edges {
|
||||
append_feature_edge(
|
||||
&mut set.edge_verts,
|
||||
&mut set.edge_verts_low,
|
||||
&vertices,
|
||||
a,
|
||||
b,
|
||||
);
|
||||
}
|
||||
Some(set)
|
||||
}
|
||||
|
||||
/// Convert every standard DWG mesh family to the material-aware shaded mesh
|
||||
/// pipeline. The retained wire entity carries snaps/grips only; fill, normals,
|
||||
/// depth, materials and face colours live in `MeshLodSet`.
|
||||
pub(crate) fn tessellate_shaded_mesh(
|
||||
entity: &acadrust::EntityType,
|
||||
color: [f32; 4],
|
||||
) -> Option<crate::scene::model::mesh_model::MeshLodSet> {
|
||||
use std::collections::HashSet;
|
||||
|
||||
match entity {
|
||||
acadrust::EntityType::Mesh(mesh) => {
|
||||
let display = display_mesh(mesh);
|
||||
let mut edges = HashSet::new();
|
||||
for face in &display.faces {
|
||||
for corner in 0..face.len() {
|
||||
edges.insert(edge_key(face[corner], face[(corner + 1) % face.len()]));
|
||||
}
|
||||
}
|
||||
make_mesh_lod_set(
|
||||
mesh.common.handle.value().to_string(),
|
||||
color,
|
||||
display.vertices,
|
||||
display.faces,
|
||||
edges,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
acadrust::EntityType::PolygonMesh(mesh) => {
|
||||
let m = mesh.m_vertex_count.max(0) as usize;
|
||||
let n = mesh.n_vertex_count.max(0) as usize;
|
||||
if m == 0 || n == 0 || mesh.vertices.len() < m.saturating_mul(n) {
|
||||
return None;
|
||||
}
|
||||
let vertices: Vec<[f64; 3]> = mesh
|
||||
.vertices
|
||||
.iter()
|
||||
.take(m * n)
|
||||
.map(|vertex| {
|
||||
[
|
||||
vertex.location.x,
|
||||
vertex.location.y,
|
||||
vertex.location.z,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let closed_m = mesh.is_closed_m();
|
||||
let closed_n = mesh.is_closed_n();
|
||||
let m_cells = if closed_m { m } else { m.saturating_sub(1) };
|
||||
let n_cells = if closed_n { n } else { n.saturating_sub(1) };
|
||||
let mut faces = Vec::with_capacity(m_cells.saturating_mul(n_cells));
|
||||
let mut edges = HashSet::new();
|
||||
for i in 0..m_cells {
|
||||
for j in 0..n_cells {
|
||||
let face = vec![
|
||||
i * n + j,
|
||||
((i + 1) % m) * n + j,
|
||||
((i + 1) % m) * n + (j + 1) % n,
|
||||
i * n + (j + 1) % n,
|
||||
];
|
||||
for corner in 0..4 {
|
||||
edges.insert(edge_key(face[corner], face[(corner + 1) % 4]));
|
||||
}
|
||||
faces.push(face);
|
||||
}
|
||||
}
|
||||
make_mesh_lod_set(
|
||||
mesh.common.handle.value().to_string(),
|
||||
color,
|
||||
vertices,
|
||||
faces,
|
||||
edges,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
acadrust::EntityType::PolyfaceMesh(mesh) => {
|
||||
let vertices: Vec<[f64; 3]> = mesh
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
[
|
||||
vertex.location.x,
|
||||
vertex.location.y,
|
||||
vertex.location.z,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let mut faces = Vec::new();
|
||||
let mut face_colors = Vec::new();
|
||||
let mut edges = HashSet::new();
|
||||
for face in &mesh.faces {
|
||||
let raw = [face.index1, face.index2, face.index3, face.index4];
|
||||
let indices: Vec<usize> = raw
|
||||
.iter()
|
||||
.filter(|&&index| index != 0)
|
||||
.filter_map(|&index| (index.unsigned_abs() as usize).checked_sub(1))
|
||||
.filter(|&index| index < vertices.len())
|
||||
.collect();
|
||||
if indices.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
for corner in 0..indices.len() {
|
||||
if raw[corner] > 0 {
|
||||
edges.insert(edge_key(indices[corner], indices[(corner + 1) % indices.len()]));
|
||||
}
|
||||
}
|
||||
faces.push(indices);
|
||||
face_colors.push(face.color.as_ref().map(crate::scene::convert::tess_util::aci_to_rgba));
|
||||
}
|
||||
make_mesh_lod_set(
|
||||
mesh.common.handle.value().to_string(),
|
||||
color,
|
||||
vertices,
|
||||
faces,
|
||||
edges,
|
||||
&face_colors,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl TruckConvertible for Mesh {
|
||||
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
||||
if self.vertices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let get = |i: usize| -> Option<[f64; 3]> { self.vertices.get(i).map(|v| [v.x, v.y, v.z]) };
|
||||
|
||||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||||
if !self.edges.is_empty() {
|
||||
for edge in &self.edges {
|
||||
if let (Some(a), Some(b)) = (get(edge.start), get(edge.end)) {
|
||||
pts.push([f64::NAN; 3]);
|
||||
pts.push(a);
|
||||
pts.push(b);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for face in &self.faces {
|
||||
if face.vertices.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
pts.push([f64::NAN; 3]);
|
||||
for &vi in &face.vertices {
|
||||
if let Some(p) = get(vi) {
|
||||
pts.push(p);
|
||||
}
|
||||
}
|
||||
if let Some(first) = face.vertices.first().and_then(|&i| get(i)) {
|
||||
pts.push(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ear-clip each face into fill_tris so shaded views render the mesh as
|
||||
// a solid surface (fan triangulation spills outside concave faces).
|
||||
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
||||
for face in &self.faces {
|
||||
let verts: Vec<[f64; 3]> = face.vertices.iter().filter_map(|&vi| get(vi)).collect();
|
||||
if verts.len() >= 3 {
|
||||
fill_tris.extend(triangulate_planar(&verts));
|
||||
}
|
||||
}
|
||||
|
||||
// Per-vertex snap tables cost ~56 B/vertex and are retained in every
|
||||
// copy of the wire set. On NWD-scale imports (#358: tens of millions
|
||||
|
|
@ -705,11 +1095,11 @@ impl TruckConvertible for Mesh {
|
|||
|
||||
Some(TruckEntity {
|
||||
pick_tris: Vec::new(),
|
||||
object: TruckObject::Lines(pts),
|
||||
object: TruckObject::Lines(Vec::new()),
|
||||
snap_pts,
|
||||
tangent_geoms: vec![],
|
||||
key_vertices,
|
||||
fill_tris,
|
||||
fill_tris: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -761,12 +1151,36 @@ impl PropertyEditable for Mesh {
|
|||
vec![PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro(
|
||||
"Level of Smoothness",
|
||||
"msh_subdiv",
|
||||
self.subdivision_level.to_string(),
|
||||
),
|
||||
Property {
|
||||
label: "Level of Smoothness".into(),
|
||||
field: "msh_subdiv_edit",
|
||||
value: PropValue::EditText(self.subdivision_level.to_string()),
|
||||
},
|
||||
Property {
|
||||
label: "Blend Creases".into(),
|
||||
field: "msh_blend_crease",
|
||||
value: PropValue::BoolToggle {
|
||||
field: "msh_blend_crease",
|
||||
value: self.blend_crease,
|
||||
},
|
||||
},
|
||||
ro("Number of Faces", "msh_f", self.faces.len().to_string()),
|
||||
ro("Number of Vertices", "msh_v", self.vertices.len().to_string()),
|
||||
ro("Number of Edges", "msh_e", self.edges.len().to_string()),
|
||||
ro(
|
||||
"Creased Edges",
|
||||
"msh_creased",
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|edge| edge.crease.is_some_and(|value| value > 0.0))
|
||||
.count()
|
||||
.to_string(),
|
||||
),
|
||||
ro(
|
||||
"Override Option",
|
||||
"msh_override",
|
||||
self.override_option.to_string(),
|
||||
),
|
||||
ro("Number of Grips", "msh_grips", self.vertices.len().to_string()),
|
||||
ro(
|
||||
"Watertight",
|
||||
|
|
@ -777,7 +1191,24 @@ impl PropertyEditable for Mesh {
|
|||
}]
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match field {
|
||||
"msh_subdiv_edit" => {
|
||||
if let Some(value) = parse_f64(value) {
|
||||
self.subdivision_level = (value.round() as i32).clamp(0, 4);
|
||||
}
|
||||
}
|
||||
"msh_blend_crease" => {
|
||||
self.blend_crease = match value {
|
||||
"toggle" => !self.blend_crease,
|
||||
"On" | "Yes" | "true" | "1" => true,
|
||||
"Off" | "No" | "false" | "0" => false,
|
||||
_ => self.blend_crease,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for Mesh {
|
||||
|
|
@ -789,40 +1220,3 @@ impl Transformable for Mesh {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod triangulate_tests {
|
||||
use super::triangulate_planar;
|
||||
|
||||
fn poly_area2d(p: &[[f64; 2]]) -> f64 {
|
||||
let n = p.len();
|
||||
let mut a = 0.0;
|
||||
for i in 0..n {
|
||||
let b = p[(i + 1) % n];
|
||||
a += p[i][0] * b[1] - b[0] * p[i][1];
|
||||
}
|
||||
(a * 0.5).abs()
|
||||
}
|
||||
fn tri_area(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
|
||||
// area in XY plane (test polygons are in Z=0)
|
||||
(((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) * 0.5).abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concave_l_shape_no_spillover() {
|
||||
// L-shaped hexagon (concave at the inner corner).
|
||||
let poly: Vec<[f64; 3]> = vec![
|
||||
[0.0, 0.0, 0.0], [4.0, 0.0, 0.0], [4.0, 2.0, 0.0],
|
||||
[2.0, 2.0, 0.0], [2.0, 4.0, 0.0], [0.0, 4.0, 0.0],
|
||||
];
|
||||
let p2: Vec<[f64; 2]> = poly.iter().map(|p| [p[0], p[1]]).collect();
|
||||
let want = poly_area2d(&p2); // = 12.0
|
||||
let tris = triangulate_planar(&poly);
|
||||
assert_eq!(tris.len() % 3, 0);
|
||||
assert_eq!(tris.len() / 3, poly.len() - 2, "expected n-2 triangles");
|
||||
let got: f64 = tris.chunks(3).map(|t| tri_area(t[0], t[1], t[2])).sum();
|
||||
eprintln!("concave L: want_area={want} got_area={got} tris={}", tris.len() / 3);
|
||||
// Fan would overshoot (triangles outside the L). Ear clip = exact.
|
||||
assert!((got - want).abs() < 1e-6, "triangle area {got} != polygon area {want} (spillover)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub mod common;
|
|||
pub mod dim_override;
|
||||
pub mod dimension;
|
||||
pub mod ellipse;
|
||||
pub mod extended;
|
||||
pub mod field;
|
||||
pub mod hatch;
|
||||
pub mod helix;
|
||||
|
|
@ -34,3 +35,4 @@ pub mod tolerance;
|
|||
pub mod traits;
|
||||
pub mod underlay;
|
||||
pub mod viewport;
|
||||
pub mod view_symbol;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
// the MeshModel vertices to match.
|
||||
|
||||
use acadrust::entities::{Body, Region, Solid3D, Surface};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{center_grip, edit_prop as edit, parse_f64, ro_prop as ro};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||
|
|
@ -42,13 +40,166 @@ fn dvec3(v: &acadrust::types::Vector3) -> glam::DVec3 {
|
|||
glam::DVec3::new(v.x, v.y, v.z)
|
||||
}
|
||||
|
||||
fn translate_wires(wires: &mut Vec<acadrust::entities::Wire>, d: Vec3) {
|
||||
for wire in wires.iter_mut() {
|
||||
for pt in wire.points.iter_mut() {
|
||||
pt.x += d.x as f64;
|
||||
pt.y += d.y as f64;
|
||||
pt.z += d.z as f64;
|
||||
}
|
||||
fn translate_acis_entity<T: acadrust::Entity>(entity: &mut T, d: glam::DVec3) {
|
||||
acadrust::Entity::translate(
|
||||
entity,
|
||||
acadrust::types::Vector3::new(d.x, d.y, d.z),
|
||||
);
|
||||
}
|
||||
|
||||
fn yes_no(value: bool) -> &'static str {
|
||||
if value { "Yes" } else { "No" }
|
||||
}
|
||||
|
||||
fn handle_text(handle: Option<acadrust::Handle>) -> String {
|
||||
handle
|
||||
.filter(|handle| handle.is_valid())
|
||||
.map(|handle| format!("{:X}", handle.value()))
|
||||
.unwrap_or_else(|| "None".to_string())
|
||||
}
|
||||
|
||||
fn acis_sections(
|
||||
acis: &acadrust::entities::AcisData,
|
||||
wires: &[acadrust::entities::Wire],
|
||||
silhouettes: &[acadrust::entities::Silhouette],
|
||||
history: Option<acadrust::Handle>,
|
||||
) -> Vec<PropSection> {
|
||||
let mut wire_types = [0usize; 5];
|
||||
let mut transformed = 0usize;
|
||||
let mut points = 0usize;
|
||||
for wire in wires {
|
||||
let index = match wire.wire_type {
|
||||
acadrust::entities::WireType::Silhouette => 1,
|
||||
acadrust::entities::WireType::VisibleEdge => 2,
|
||||
acadrust::entities::WireType::HiddenEdge => 3,
|
||||
acadrust::entities::WireType::Isoline => 4,
|
||||
_ => 0,
|
||||
};
|
||||
wire_types[index] += 1;
|
||||
transformed += wire.has_transform as usize;
|
||||
points += wire.points.len();
|
||||
}
|
||||
let revision = if acis.revision.has_guid {
|
||||
format!(
|
||||
"{}-{}-{}-{:02X?}; end {}",
|
||||
acis.revision.major,
|
||||
acis.revision.minor1,
|
||||
acis.revision.minor2,
|
||||
acis.revision.bytes,
|
||||
acis.revision.end_marker
|
||||
)
|
||||
} else {
|
||||
"None".to_string()
|
||||
};
|
||||
let bindings = if acis.materials.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
acis.materials
|
||||
.iter()
|
||||
.map(|binding| {
|
||||
format!(
|
||||
"{}:{}→{}",
|
||||
binding.array_index,
|
||||
binding.absolute_reference,
|
||||
handle_text(binding.material_handle)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
};
|
||||
let extra = acis.extra_acis_data.as_ref().map_or_else(
|
||||
|| "None".to_string(),
|
||||
|data| {
|
||||
format!(
|
||||
"{}; {} bytes",
|
||||
if data.is_binary { "SAB" } else { "SAT" },
|
||||
data.size()
|
||||
)
|
||||
},
|
||||
);
|
||||
vec![
|
||||
PropSection {
|
||||
title: "Modeler Geometry".into(),
|
||||
props: vec![
|
||||
ro(
|
||||
"Format",
|
||||
"acis_format",
|
||||
if acis.is_binary { "SAB" } else { "SAT" },
|
||||
),
|
||||
ro("Version", "acis_version", format!("{:?}", acis.version)),
|
||||
ro("Data Size", "acis_size", format!("{} bytes", acis.size())),
|
||||
ro("Revision", "acis_revision", revision),
|
||||
ro("Material Bindings", "acis_materials", bindings),
|
||||
ro("Extra Modeler Data", "acis_extra", extra),
|
||||
ro("History", "acis_history", handle_text(history)),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: "Modeler Display Data".into(),
|
||||
props: vec![
|
||||
ro(
|
||||
"Wireframe Cache",
|
||||
"acis_wireframe",
|
||||
yes_no(acis.wireframe_data_present),
|
||||
),
|
||||
ro(
|
||||
"Reference Point",
|
||||
"acis_wireframe_point",
|
||||
yes_no(acis.wireframe_point_present),
|
||||
),
|
||||
ro(
|
||||
"Isoline List",
|
||||
"acis_wireframe_isolines",
|
||||
yes_no(acis.wireframe_isoline_present),
|
||||
),
|
||||
ro(
|
||||
"Isolines",
|
||||
"acis_isolines",
|
||||
acis.wireframe_isolines.to_string(),
|
||||
),
|
||||
ro(
|
||||
"Wires",
|
||||
"acis_wires",
|
||||
format!(
|
||||
"{} (unknown {}, silhouette {}, visible {}, hidden {}, isoline {}; {} points; {} transformed)",
|
||||
wires.len(),
|
||||
wire_types[0],
|
||||
wire_types[1],
|
||||
wire_types[2],
|
||||
wire_types[3],
|
||||
wire_types[4],
|
||||
points,
|
||||
transformed
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"View Silhouettes",
|
||||
"acis_silhouettes",
|
||||
format!(
|
||||
"{} views; {} wires",
|
||||
silhouettes.len(),
|
||||
silhouettes.iter().map(|silhouette| silhouette.wires.len()).sum::<usize>()
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn position_section(prefix: &str, p: &acadrust::types::Vector3) -> PropSection {
|
||||
let fields = match prefix {
|
||||
"rgn" => ["rgn_px", "rgn_py", "rgn_pz"],
|
||||
"bdy" => ["bdy_px", "bdy_py", "bdy_pz"],
|
||||
"srf" => ["srf_px", "srf_py", "srf_pz"],
|
||||
_ => ["s3d_px", "s3d_py", "s3d_pz"],
|
||||
};
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Position X", fields[0], p.x),
|
||||
edit("Position Y", fields[1], p.y),
|
||||
edit("Position Z", fields[2], p.z),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,54 +248,34 @@ impl Grippable for Solid3D {
|
|||
return;
|
||||
}
|
||||
if let GripApply::Translate(d) = apply {
|
||||
self.point_of_reference.x += d.x as f64;
|
||||
self.point_of_reference.y += d.y as f64;
|
||||
self.point_of_reference.z += d.z as f64;
|
||||
translate_wires(&mut self.wires, d.as_vec3());
|
||||
translate_acis_entity(self, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Solid3D {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
let has_history = matches!(self.history_handle, Some(h) if !h.is_null());
|
||||
let history = if has_history {
|
||||
format!("{:X}", self.history_handle.unwrap().value())
|
||||
} else {
|
||||
"None".to_string()
|
||||
};
|
||||
vec![
|
||||
PropSection {
|
||||
title: "Solid History".into(),
|
||||
props: vec![
|
||||
ro("History", "s3d_history", history),
|
||||
ro(
|
||||
"Show History",
|
||||
"s3d_show_history",
|
||||
if has_history { "Yes" } else { "No" },
|
||||
),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
edit("Position X", "s3d_px", self.point_of_reference.x),
|
||||
edit("Position Y", "s3d_py", self.point_of_reference.y),
|
||||
edit("Position Z", "s3d_pz", self.point_of_reference.z),
|
||||
],
|
||||
},
|
||||
]
|
||||
let mut sections =
|
||||
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
|
||||
sections[0]
|
||||
.props
|
||||
.insert(0, ro("UID", "s3d_uid", self.uid.clone()));
|
||||
sections.push(position_section("s3d", &self.point_of_reference));
|
||||
sections
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"s3d_px" => self.point_of_reference.x = v,
|
||||
"s3d_py" => self.point_of_reference.y = v,
|
||||
"s3d_pz" => self.point_of_reference.z = v,
|
||||
_ => {}
|
||||
let delta = match field {
|
||||
"s3d_px" => acadrust::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0),
|
||||
"s3d_py" => acadrust::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0),
|
||||
"s3d_pz" => acadrust::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z),
|
||||
_ => return,
|
||||
};
|
||||
if delta != acadrust::types::Vector3::ZERO {
|
||||
acadrust::Entity::translate(self, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,10 +292,7 @@ impl Grippable for Region {
|
|||
return;
|
||||
}
|
||||
if let GripApply::Translate(d) = apply {
|
||||
self.point_of_reference.x += d.x as f64;
|
||||
self.point_of_reference.y += d.y as f64;
|
||||
self.point_of_reference.z += d.z as f64;
|
||||
translate_wires(&mut self.wires, d.as_vec3());
|
||||
translate_acis_entity(self, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,16 +300,36 @@ impl Grippable for Region {
|
|||
impl PropertyEditable for Region {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
let (area, perimeter) = region_area_perimeter(&self.wires);
|
||||
vec![PropSection {
|
||||
title: "Geometry".into(),
|
||||
props: vec![
|
||||
ro("Area", "rgn_area", format!("{area:.4}")),
|
||||
ro("Perimeter", "rgn_perimeter", format!("{perimeter:.4}")),
|
||||
],
|
||||
}]
|
||||
let mut sections =
|
||||
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
|
||||
sections[0]
|
||||
.props
|
||||
.insert(0, ro("UID", "rgn_uid", self.uid.clone()));
|
||||
let mut geometry = position_section("rgn", &self.point_of_reference);
|
||||
geometry
|
||||
.props
|
||||
.push(ro("Area", "rgn_area", format!("{area:.4}")));
|
||||
geometry
|
||||
.props
|
||||
.push(ro("Perimeter", "rgn_perimeter", format!("{perimeter:.4}")));
|
||||
sections.push(geometry);
|
||||
sections
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
let delta = match field {
|
||||
"rgn_px" => acadrust::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0),
|
||||
"rgn_py" => acadrust::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0),
|
||||
"rgn_pz" => acadrust::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z),
|
||||
_ => return,
|
||||
};
|
||||
if delta != acadrust::types::Vector3::ZERO {
|
||||
acadrust::Entity::translate(self, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Body ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -196,36 +344,458 @@ impl Grippable for Body {
|
|||
return;
|
||||
}
|
||||
if let GripApply::Translate(d) = apply {
|
||||
self.point_of_reference.x += d.x as f64;
|
||||
self.point_of_reference.y += d.y as f64;
|
||||
self.point_of_reference.z += d.z as f64;
|
||||
translate_wires(&mut self.wires, d.as_vec3());
|
||||
translate_acis_entity(self, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Body {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
let has_history = matches!(self.history_handle, Some(h) if !h.is_null());
|
||||
let history = if has_history {
|
||||
format!("{:X}", self.history_handle.unwrap().value())
|
||||
} else {
|
||||
"None".to_string()
|
||||
};
|
||||
vec![PropSection {
|
||||
title: "Solid History".into(),
|
||||
props: vec![
|
||||
ro("History", "bdy_history", history),
|
||||
ro(
|
||||
"Show History",
|
||||
"bdy_show_history",
|
||||
if has_history { "Yes" } else { "No" },
|
||||
),
|
||||
],
|
||||
}]
|
||||
let mut sections =
|
||||
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
|
||||
sections[0]
|
||||
.props
|
||||
.insert(0, ro("UID", "bdy_uid", self.uid.clone()));
|
||||
sections.push(position_section("bdy", &self.point_of_reference));
|
||||
sections
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Some(v) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
let delta = match field {
|
||||
"bdy_px" => acadrust::types::Vector3::new(v - self.point_of_reference.x, 0.0, 0.0),
|
||||
"bdy_py" => acadrust::types::Vector3::new(0.0, v - self.point_of_reference.y, 0.0),
|
||||
"bdy_pz" => acadrust::types::Vector3::new(0.0, 0.0, v - self.point_of_reference.z),
|
||||
_ => return,
|
||||
};
|
||||
if delta != acadrust::types::Vector3::ZERO {
|
||||
acadrust::Entity::translate(self, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn embedded_name(entity: Option<&acadrust::entities::EmbeddedEntity>) -> &'static str {
|
||||
match entity {
|
||||
Some(acadrust::entities::EmbeddedEntity::Point(_)) => "Point",
|
||||
Some(acadrust::entities::EmbeddedEntity::Line(_)) => "Line",
|
||||
Some(acadrust::entities::EmbeddedEntity::Arc(_)) => "Arc",
|
||||
Some(acadrust::entities::EmbeddedEntity::Circle(_)) => "Circle",
|
||||
Some(acadrust::entities::EmbeddedEntity::Ellipse(_)) => "Ellipse",
|
||||
Some(acadrust::entities::EmbeddedEntity::Spline(_)) => "Spline",
|
||||
Some(acadrust::entities::EmbeddedEntity::LwPolyline(_)) => "Polyline",
|
||||
Some(acadrust::entities::EmbeddedEntity::Ray(_)) => "Ray",
|
||||
Some(acadrust::entities::EmbeddedEntity::XLine(_)) => "XLine",
|
||||
Some(acadrust::entities::EmbeddedEntity::Unknown { .. }) => "Unknown",
|
||||
None => "None",
|
||||
}
|
||||
}
|
||||
|
||||
fn vector_text(vector: &acadrust::types::Vector3) -> String {
|
||||
format!("{:.6}, {:.6}, {:.6}", vector.x, vector.y, vector.z)
|
||||
}
|
||||
|
||||
fn matrix_text(matrix: &[f64; 16]) -> String {
|
||||
matrix
|
||||
.chunks_exact(4)
|
||||
.map(|row| {
|
||||
format!(
|
||||
"[{:.4}, {:.4}, {:.4}, {:.4}]",
|
||||
row[0], row[1], row[2], row[3]
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn sweep_options_text(options: &acadrust::entities::SurfaceSweepOptions) -> String {
|
||||
format!(
|
||||
"draft {:.6} ({:.6}→{:.6}); twist {:.6}; scale {:.6}; align {:.6}; solid {}; flags {}/{}; align-start {}; bank {}; base {}; sweep-xform {}; path-xform {}; ref {}",
|
||||
options.draft_angle,
|
||||
options.draft_start_distance,
|
||||
options.draft_end_distance,
|
||||
options.twist_angle,
|
||||
options.scale_factor,
|
||||
options.align_angle,
|
||||
yes_no(options.is_solid),
|
||||
options.sweep_alignment_flags,
|
||||
options.path_flags,
|
||||
yes_no(options.align_start),
|
||||
yes_no(options.bank),
|
||||
yes_no(options.base_point_set),
|
||||
yes_no(options.sweep_entity_transform_computed),
|
||||
yes_no(options.path_entity_transform_computed),
|
||||
vector_text(&options.reference_vector)
|
||||
)
|
||||
}
|
||||
|
||||
fn surface_construction_section(surface: &Surface) -> PropSection {
|
||||
use acadrust::entities::SurfaceData;
|
||||
let mut props = vec![
|
||||
ro("Kind", "srf_kind", format!("{:?}", surface.kind)),
|
||||
ro(
|
||||
"Modeler Format",
|
||||
"srf_modeler_version",
|
||||
surface.modeler_format_version.to_string(),
|
||||
),
|
||||
edit(
|
||||
"U Isolines",
|
||||
"srf_u_isolines",
|
||||
surface.u_isolines as f64,
|
||||
),
|
||||
edit(
|
||||
"V Isolines",
|
||||
"srf_v_isolines",
|
||||
surface.v_isolines as f64,
|
||||
),
|
||||
];
|
||||
match &surface.surface_data {
|
||||
SurfaceData::Generic => {}
|
||||
SurfaceData::Plane { class_version } => {
|
||||
props.push(ro(
|
||||
"Class Version",
|
||||
"srf_class_version",
|
||||
class_version.to_string(),
|
||||
));
|
||||
}
|
||||
SurfaceData::Extruded {
|
||||
sweep_entity,
|
||||
options,
|
||||
sweep_vector,
|
||||
sweep_transform,
|
||||
} => {
|
||||
props.extend([
|
||||
ro(
|
||||
"Sweep Entity",
|
||||
"srf_sweep_entity",
|
||||
embedded_name(sweep_entity.as_ref()),
|
||||
),
|
||||
ro("Sweep Vector", "srf_sweep_vector", vector_text(sweep_vector)),
|
||||
ro("Sweep Options", "srf_sweep_options", sweep_options_text(options)),
|
||||
ro(
|
||||
"Sweep Transform",
|
||||
"srf_sweep_transform",
|
||||
matrix_text(sweep_transform),
|
||||
),
|
||||
ro(
|
||||
"Sweep Entity Transform",
|
||||
"srf_sweep_entity_transform",
|
||||
matrix_text(&options.sweep_entity_transform),
|
||||
),
|
||||
ro(
|
||||
"Path Entity Transform",
|
||||
"srf_path_entity_transform",
|
||||
matrix_text(&options.path_entity_transform),
|
||||
),
|
||||
]);
|
||||
}
|
||||
SurfaceData::Lofted {
|
||||
loft_transform,
|
||||
cross_section_entities,
|
||||
guide_entities,
|
||||
path_entity,
|
||||
plane_normal_lofting_type,
|
||||
start_draft_angle,
|
||||
end_draft_angle,
|
||||
start_draft_magnitude,
|
||||
end_draft_magnitude,
|
||||
arc_length_parameterization,
|
||||
no_twist,
|
||||
align_direction,
|
||||
simple_surfaces,
|
||||
closed_surfaces,
|
||||
solid,
|
||||
ruled_surface,
|
||||
virtual_guide,
|
||||
cross_sections,
|
||||
guide_curves,
|
||||
path_curve,
|
||||
} => {
|
||||
props.extend([
|
||||
ro(
|
||||
"Embedded Curves",
|
||||
"srf_loft_embedded",
|
||||
format!(
|
||||
"{} cross sections; {} guides; path {}",
|
||||
cross_section_entities.len(),
|
||||
guide_entities.len(),
|
||||
embedded_name(path_entity.as_ref())
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Database Curves",
|
||||
"srf_loft_handles",
|
||||
format!(
|
||||
"{} cross sections; {} guides; path {}",
|
||||
cross_sections.len(),
|
||||
guide_curves.len(),
|
||||
handle_text(*path_curve)
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Draft",
|
||||
"srf_loft_draft",
|
||||
format!(
|
||||
"type {}; angle {:.6}→{:.6}; magnitude {:.6}→{:.6}",
|
||||
plane_normal_lofting_type,
|
||||
start_draft_angle,
|
||||
end_draft_angle,
|
||||
start_draft_magnitude,
|
||||
end_draft_magnitude
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Loft Flags",
|
||||
"srf_loft_flags",
|
||||
format!(
|
||||
"arc-length {}; no-twist {}; align {}; simple {}; closed {}; solid {}; ruled {}; virtual-guide {}",
|
||||
yes_no(*arc_length_parameterization),
|
||||
yes_no(*no_twist),
|
||||
yes_no(*align_direction),
|
||||
yes_no(*simple_surfaces),
|
||||
yes_no(*closed_surfaces),
|
||||
yes_no(*solid),
|
||||
yes_no(*ruled_surface),
|
||||
yes_no(*virtual_guide)
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Loft Transform",
|
||||
"srf_loft_transform",
|
||||
matrix_text(loft_transform),
|
||||
),
|
||||
]);
|
||||
}
|
||||
SurfaceData::Revolved {
|
||||
revolve_entity,
|
||||
class_version,
|
||||
entity_id,
|
||||
axis_point,
|
||||
axis_vector,
|
||||
revolve_angle,
|
||||
start_angle,
|
||||
entity_transform,
|
||||
draft_angle,
|
||||
draft_start_distance,
|
||||
draft_end_distance,
|
||||
twist_angle,
|
||||
solid,
|
||||
close_to_axis,
|
||||
} => {
|
||||
props.extend([
|
||||
ro(
|
||||
"Revolve Entity",
|
||||
"srf_revolve_entity",
|
||||
embedded_name(revolve_entity.as_ref()),
|
||||
),
|
||||
ro(
|
||||
"Class / Entity",
|
||||
"srf_revolve_ids",
|
||||
format!("{class_version} / {entity_id}"),
|
||||
),
|
||||
ro("Axis Point", "srf_axis_point", vector_text(axis_point)),
|
||||
ro("Axis Vector", "srf_axis_vector", vector_text(axis_vector)),
|
||||
ro(
|
||||
"Angles",
|
||||
"srf_revolve_angles",
|
||||
format!(
|
||||
"start {:.6}; revolve {:.6}; draft {:.6}; twist {:.6}",
|
||||
start_angle, revolve_angle, draft_angle, twist_angle
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Draft Distances",
|
||||
"srf_revolve_draft_distances",
|
||||
format!("{draft_start_distance:.6}→{draft_end_distance:.6}"),
|
||||
),
|
||||
ro(
|
||||
"Revolve Flags",
|
||||
"srf_revolve_flags",
|
||||
format!(
|
||||
"solid {}; close-to-axis {}",
|
||||
yes_no(*solid),
|
||||
yes_no(*close_to_axis)
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Entity Transform",
|
||||
"srf_revolve_transform",
|
||||
matrix_text(entity_transform),
|
||||
),
|
||||
]);
|
||||
}
|
||||
SurfaceData::Swept {
|
||||
class_version,
|
||||
sweep_entity,
|
||||
path_entity,
|
||||
sweep_transform,
|
||||
path_transform,
|
||||
options,
|
||||
} => {
|
||||
props.extend([
|
||||
ro(
|
||||
"Class Version",
|
||||
"srf_class_version",
|
||||
class_version.to_string(),
|
||||
),
|
||||
ro(
|
||||
"Sweep / Path",
|
||||
"srf_swept_entities",
|
||||
format!(
|
||||
"{} / {}",
|
||||
embedded_name(sweep_entity.as_ref()),
|
||||
embedded_name(path_entity.as_ref())
|
||||
),
|
||||
),
|
||||
ro("Sweep Options", "srf_sweep_options", sweep_options_text(options)),
|
||||
ro(
|
||||
"Sweep Transform",
|
||||
"srf_sweep_transform",
|
||||
matrix_text(sweep_transform),
|
||||
),
|
||||
ro(
|
||||
"Path Transform",
|
||||
"srf_path_transform",
|
||||
matrix_text(path_transform),
|
||||
),
|
||||
ro(
|
||||
"Sweep Entity Transform",
|
||||
"srf_sweep_entity_transform",
|
||||
matrix_text(&options.sweep_entity_transform),
|
||||
),
|
||||
ro(
|
||||
"Path Entity Transform",
|
||||
"srf_path_entity_transform",
|
||||
matrix_text(&options.path_entity_transform),
|
||||
),
|
||||
]);
|
||||
}
|
||||
SurfaceData::Nurb {
|
||||
short_170,
|
||||
cv_hull_display,
|
||||
u_vector1,
|
||||
v_vector1,
|
||||
u_vector2,
|
||||
v_vector2,
|
||||
} => {
|
||||
props.extend([
|
||||
ro(
|
||||
"NURB Version",
|
||||
"srf_nurb_version",
|
||||
short_170.to_string(),
|
||||
),
|
||||
ro(
|
||||
"CV Hull",
|
||||
"srf_nurb_cv_hull",
|
||||
yes_no(*cv_hull_display),
|
||||
),
|
||||
ro("U Vector 1", "srf_nurb_u1", vector_text(u_vector1)),
|
||||
ro("V Vector 1", "srf_nurb_v1", vector_text(v_vector1)),
|
||||
ro("U Vector 2", "srf_nurb_u2", vector_text(u_vector2)),
|
||||
ro("V Vector 2", "srf_nurb_v2", vector_text(v_vector2)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
props.extend([
|
||||
ro(
|
||||
"Raw DWG Body",
|
||||
"srf_raw_dwg",
|
||||
surface.raw_dwg_data.as_ref().map_or_else(
|
||||
|| "None".to_string(),
|
||||
|bytes| format!("{} bytes; {} handle bits", bytes.len(), surface.dwg_handle_bits),
|
||||
),
|
||||
),
|
||||
ro(
|
||||
"Raw Source Version",
|
||||
"srf_raw_version",
|
||||
surface
|
||||
.dwg_source_version
|
||||
.map(|version| format!("{version:?}"))
|
||||
.unwrap_or_else(|| "None".to_string()),
|
||||
),
|
||||
]);
|
||||
PropSection {
|
||||
title: "Surface Construction".into(),
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_surface(surface: &mut Surface, delta: acadrust::types::Vector3) {
|
||||
let before = surface.point_of_reference;
|
||||
acadrust::Entity::translate(surface, delta);
|
||||
if surface.point_of_reference == before {
|
||||
surface.point_of_reference = before + delta;
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for Surface {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
vec![center_grip(0, dvec3(&self.point_of_reference))]
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
if grip_id != 0 {
|
||||
return;
|
||||
}
|
||||
if let GripApply::Translate(delta) = apply {
|
||||
translate_surface(
|
||||
self,
|
||||
acadrust::types::Vector3::new(delta.x, delta.y, delta.z),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for Surface {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
let mut sections =
|
||||
acis_sections(&self.acis_data, &self.wires, &self.silhouettes, self.history_handle);
|
||||
sections.push(surface_construction_section(self));
|
||||
sections.push(position_section("srf", &self.point_of_reference));
|
||||
sections
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
match field {
|
||||
"srf_u_isolines" => {
|
||||
if let Some(value) = parse_f64(value) {
|
||||
self.u_isolines = (value.round() as i16).max(0);
|
||||
}
|
||||
}
|
||||
"srf_v_isolines" => {
|
||||
if let Some(value) = parse_f64(value) {
|
||||
self.v_isolines = (value.round() as i16).max(0);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let Some(value) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
let delta = match field {
|
||||
"srf_px" => acadrust::types::Vector3::new(
|
||||
value - self.point_of_reference.x,
|
||||
0.0,
|
||||
0.0,
|
||||
),
|
||||
"srf_py" => acadrust::types::Vector3::new(
|
||||
0.0,
|
||||
value - self.point_of_reference.y,
|
||||
0.0,
|
||||
),
|
||||
"srf_pz" => acadrust::types::Vector3::new(
|
||||
0.0,
|
||||
0.0,
|
||||
value - self.point_of_reference.z,
|
||||
),
|
||||
_ => return,
|
||||
};
|
||||
if delta != acadrust::types::Vector3::ZERO {
|
||||
translate_surface(self, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Accessors for the Solid3D / Region / Body trio ─────────────────────────
|
||||
|
|
@ -246,6 +816,7 @@ pub fn point_of_reference(e: &EntityType) -> Option<&Vector3> {
|
|||
EntityType::Solid3D(s) => Some(&s.point_of_reference),
|
||||
EntityType::Region(r) => Some(&r.point_of_reference),
|
||||
EntityType::Body(b) => Some(&b.point_of_reference),
|
||||
EntityType::Surface(s) => Some(&s.point_of_reference),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -263,6 +834,32 @@ pub fn fallback_wires(e: &EntityType) -> Option<&[acadrust::entities::Wire]> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn wire_point(
|
||||
wire: &acadrust::entities::Wire,
|
||||
point: &acadrust::types::Vector3,
|
||||
) -> acadrust::types::Vector3 {
|
||||
if !wire.has_transform {
|
||||
return *point;
|
||||
}
|
||||
let x = point.x * wire.scale.x;
|
||||
let y = point.y * wire.scale.y;
|
||||
let z = point.z * wire.scale.z;
|
||||
acadrust::types::Vector3::new(
|
||||
wire.translation.x
|
||||
+ wire.x_axis.x * x
|
||||
+ wire.y_axis.x * y
|
||||
+ wire.z_axis.x * z,
|
||||
wire.translation.y
|
||||
+ wire.x_axis.y * x
|
||||
+ wire.y_axis.y * y
|
||||
+ wire.z_axis.y * z,
|
||||
wire.translation.z
|
||||
+ wire.x_axis.z * x
|
||||
+ wire.y_axis.z * y
|
||||
+ wire.z_axis.z * z,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether every ACIS face uses a surface family the mesh pipeline can decode.
|
||||
/// Unsupported or unresolved faces must keep their display-cache wires visible;
|
||||
/// otherwise a parseable but incomplete shell looks like a valid solid.
|
||||
|
|
@ -288,13 +885,15 @@ pub fn acis_has_complete_surface_support(e: &EntityType) -> bool {
|
|||
| "sphere-surface"
|
||||
| "torus-surface"
|
||||
| "spline-surface"
|
||||
| "meshsurf-surface"
|
||||
| "bs3-surface"
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the appropriate `solid3d_tess::tessellate_*` for the entity,
|
||||
/// returning `None` for non-volume entities or when the kernel fails.
|
||||
/// Build material-aware shaded geometry for every standard 3-D solid/surface
|
||||
/// and mesh family, returning `None` when decoded geometry is unusable.
|
||||
pub fn tessellate_volume(
|
||||
e: &EntityType,
|
||||
color: [f32; 4],
|
||||
|
|
@ -306,6 +905,9 @@ pub fn tessellate_volume(
|
|||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res, isolines),
|
||||
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color, facet_res, isolines),
|
||||
EntityType::Surface(s) => solid3d_tess::tessellate_surface(s, color, facet_res, isolines),
|
||||
EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => {
|
||||
crate::entities::mesh::tessellate_shaded_mesh(e, color)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -276,7 +276,7 @@ impl EntityTypeOps for EntityType {
|
|||
Wipeout, AttributeDefinition, AttributeEntity, MLine,
|
||||
Tolerance, Solid, Face3D, PolygonMesh, PolyfaceMesh, Mesh,
|
||||
Table, Text, MText, Leader, MultiLeader, Underlay, Shape,
|
||||
Ole2Frame, Helix, Light,
|
||||
Ole2Frame, Helix, Light, Extended,
|
||||
],
|
||||
_ => None,
|
||||
)
|
||||
|
|
@ -289,10 +289,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame, Helix, Light,
|
||||
Underlay, Shape, Ole2Frame, Helix, Light, SectionSymbol,
|
||||
ViewBorder, Extended,
|
||||
],
|
||||
_ => vec![],
|
||||
)
|
||||
|
|
@ -305,10 +306,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Hatch, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Dimension, Leader, MultiLeader, Underlay,
|
||||
Shape, Ole2Frame, Helix, Light,
|
||||
Shape, Ole2Frame, Helix, Light, SectionSymbol, ViewBorder,
|
||||
Extended,
|
||||
],
|
||||
_ => vec![],
|
||||
)
|
||||
|
|
@ -321,10 +323,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Hatch, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Dimension, Leader, MultiLeader, Underlay,
|
||||
Shape, Ole2Frame, Helix, Light,
|
||||
Shape, Ole2Frame, Helix, Light, SectionSymbol, ViewBorder,
|
||||
Extended,
|
||||
],
|
||||
_ => {},
|
||||
)
|
||||
|
|
@ -337,10 +340,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame, Helix, Light,
|
||||
Underlay, Shape, Ole2Frame, Helix, Light, SectionSymbol,
|
||||
ViewBorder, Extended,
|
||||
],
|
||||
_ => {},
|
||||
)
|
||||
|
|
@ -353,10 +357,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame,
|
||||
Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder,
|
||||
Extended,
|
||||
],
|
||||
_ => vec![],
|
||||
)
|
||||
|
|
@ -369,10 +374,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame,
|
||||
Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder,
|
||||
Extended,
|
||||
],
|
||||
_ => {},
|
||||
)
|
||||
|
|
@ -389,10 +395,11 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame,
|
||||
Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder,
|
||||
Extended,
|
||||
],
|
||||
_ => None,
|
||||
)
|
||||
|
|
@ -410,10 +417,10 @@ impl EntityTypeOps for EntityType {
|
|||
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||
Solid, Solid3D, Region, Body, Surface, Face3D, PolygonMesh,
|
||||
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||
Underlay, Shape, Ole2Frame,
|
||||
Underlay, Shape, Ole2Frame, SectionSymbol, ViewBorder,
|
||||
],
|
||||
_ => {},
|
||||
)
|
||||
|
|
@ -429,7 +436,8 @@ impl EntityTypeOps for EntityType {
|
|||
Tolerance, Solid, Face3D, PolygonMesh, PolyfaceMesh, Mesh,
|
||||
Table, MText, Point, Spline, Text, Viewport, Dimension,
|
||||
Leader, MultiLeader, Underlay, Shape, Ole2Frame,
|
||||
Solid3D, Region, Body, Surface, Helix, Light,
|
||||
Solid3D, Region, Body, Surface, Helix, Light, SectionSymbol,
|
||||
ViewBorder, Extended,
|
||||
],
|
||||
_ => {},
|
||||
)
|
||||
|
|
|
|||
263
src/entities/view_symbol.rs
Normal file
263
src/entities/view_symbol.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
use acadrust::entities::{SectionSymbol, ViewBorder};
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_angle_prop, edit_prop, parse_f64, ro_prop, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
|
||||
fn handle_text(handle: acadrust::Handle) -> String {
|
||||
if handle.is_null() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
format!("{:X}", handle.value())
|
||||
}
|
||||
}
|
||||
|
||||
fn vector_text(vector: &acadrust::types::Vector3) -> String {
|
||||
format!("{:.6}, {:.6}, {:.6}", vector.x, vector.y, vector.z)
|
||||
}
|
||||
|
||||
fn apply_entity_transform<T: acadrust::Entity>(entity: &mut T, transform: &EntityTransform) {
|
||||
crate::scene::view::transform::apply_standard_entity_transform(
|
||||
entity,
|
||||
transform,
|
||||
|entity, first, second| {
|
||||
entity.apply_transform(
|
||||
&crate::scene::view::transform::reflection_about_xy_line(first, second),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl Grippable for SectionSymbol {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
self.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
square_grip(
|
||||
index,
|
||||
glam::DVec3::new(point.point.x, point.point.y, point.point.z),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
let Some(point) = self.points.get_mut(grip_id) else {
|
||||
return;
|
||||
};
|
||||
match apply {
|
||||
GripApply::Translate(delta) => {
|
||||
point.point.x += delta.x;
|
||||
point.point.y += delta.y;
|
||||
point.point.z += delta.z;
|
||||
}
|
||||
GripApply::Absolute(position) => {
|
||||
point.point =
|
||||
acadrust::types::Vector3::new(position.x, position.y, position.z);
|
||||
}
|
||||
}
|
||||
self.sync_display_fields();
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for SectionSymbol {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
let point_data = self
|
||||
.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
format!(
|
||||
"{}: point [{}]; bulge {:.6}; label {:?}; offset [{}]; flag {}",
|
||||
index + 1,
|
||||
vector_text(&point.point),
|
||||
point.bulge,
|
||||
point.label,
|
||||
vector_text(&point.label_offset),
|
||||
point.raw_flag_280
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
vec![
|
||||
PropSection {
|
||||
title: "Section Symbol".into(),
|
||||
props: vec![
|
||||
edit_prop("Symbol Scale", "section_scale", self.symbol_scale),
|
||||
ro_prop(
|
||||
"View Symbol Version",
|
||||
"section_view_version",
|
||||
self.view_symbol_version.to_string(),
|
||||
),
|
||||
ro_prop("Version", "section_version", self.version.to_string()),
|
||||
ro_prop(
|
||||
"Style",
|
||||
"section_style",
|
||||
handle_text(self.style_handle),
|
||||
),
|
||||
ro_prop(
|
||||
"View Representation",
|
||||
"section_view_rep",
|
||||
handle_text(self.view_rep_handle),
|
||||
),
|
||||
ro_prop(
|
||||
"View Symbol Flags",
|
||||
"section_view_flags",
|
||||
self.raw_view_symbol_70.to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Point Count",
|
||||
"section_point_count",
|
||||
self.points.len().to_string(),
|
||||
),
|
||||
ro_prop(
|
||||
"Stored Point Counts",
|
||||
"section_raw_counts",
|
||||
format!(
|
||||
"{}, {}, flags {}",
|
||||
self.raw_point_count_90,
|
||||
self.raw_point_record_count,
|
||||
self.raw_flags_90
|
||||
),
|
||||
),
|
||||
ro_prop("Identifier", "section_label", self.label.clone()),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: "Section Points".into(),
|
||||
props: vec![ro_prop("Records", "section_points", point_data)],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
if field == "section_scale" {
|
||||
if let Some(value) = parse_f64(value) {
|
||||
self.symbol_scale = value.max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for SectionSymbol {
|
||||
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||
apply_entity_transform(self, transform);
|
||||
self.sync_display_fields();
|
||||
}
|
||||
}
|
||||
|
||||
impl Grippable for ViewBorder {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
vec![
|
||||
square_grip(0, glam::DVec3::new(self.min[0], self.min[1], 0.0)),
|
||||
square_grip(1, glam::DVec3::new(self.max[0], self.max[1], 0.0)),
|
||||
square_grip(2, glam::DVec3::new(self.center[0], self.center[1], 0.0)),
|
||||
]
|
||||
}
|
||||
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||
let delta = match apply {
|
||||
GripApply::Translate(delta) => delta,
|
||||
GripApply::Absolute(position) => {
|
||||
let current = match grip_id {
|
||||
0 => glam::DVec3::new(self.min[0], self.min[1], 0.0),
|
||||
1 => glam::DVec3::new(self.max[0], self.max[1], 0.0),
|
||||
2 => glam::DVec3::new(self.center[0], self.center[1], 0.0),
|
||||
_ => return,
|
||||
};
|
||||
position - current
|
||||
}
|
||||
};
|
||||
match grip_id {
|
||||
0 => {
|
||||
self.min[0] += delta.x;
|
||||
self.min[1] += delta.y;
|
||||
}
|
||||
1 => {
|
||||
self.max[0] += delta.x;
|
||||
self.max[1] += delta.y;
|
||||
}
|
||||
2 => {
|
||||
self.min[0] += delta.x;
|
||||
self.min[1] += delta.y;
|
||||
self.max[0] += delta.x;
|
||||
self.max[1] += delta.y;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
self.center = [
|
||||
(self.min[0] + self.max[0]) * 0.5,
|
||||
(self.min[1] + self.max[1]) * 0.5,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyEditable for ViewBorder {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
vec![PropSection {
|
||||
title: "Drawing View".into(),
|
||||
props: vec![
|
||||
edit_prop("Center X", "view_border_x", self.center[0]),
|
||||
edit_prop("Center Y", "view_border_y", self.center[1]),
|
||||
ro_prop(
|
||||
"Width",
|
||||
"view_border_width",
|
||||
format!("{:.6}", self.max[0] - self.min[0]),
|
||||
),
|
||||
ro_prop(
|
||||
"Height",
|
||||
"view_border_height",
|
||||
format!("{:.6}", self.max[1] - self.min[1]),
|
||||
),
|
||||
edit_prop("Scale", "view_border_scale", self.scale),
|
||||
edit_angle_prop(
|
||||
"Rotation",
|
||||
"view_border_rotation",
|
||||
self.rotation_angle.to_degrees(),
|
||||
),
|
||||
ro_prop("Version", "view_border_version", self.version.to_string()),
|
||||
ro_prop(
|
||||
"Active Viewport",
|
||||
"view_border_viewport",
|
||||
handle_text(self.active_viewport),
|
||||
),
|
||||
ro_prop(
|
||||
"Scale Object",
|
||||
"view_border_scale_handle",
|
||||
handle_text(self.scale_handle),
|
||||
),
|
||||
],
|
||||
}]
|
||||
}
|
||||
|
||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||
let Some(value) = parse_f64(value) else {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"view_border_x" => {
|
||||
let delta = value - self.center[0];
|
||||
self.min[0] += delta;
|
||||
self.max[0] += delta;
|
||||
self.center[0] = value;
|
||||
}
|
||||
"view_border_y" => {
|
||||
let delta = value - self.center[1];
|
||||
self.min[1] += delta;
|
||||
self.max[1] += delta;
|
||||
self.center[1] = value;
|
||||
}
|
||||
"view_border_scale" => self.scale = value.max(0.0),
|
||||
"view_border_rotation" => self.rotation_angle = value.to_radians(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transformable for ViewBorder {
|
||||
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||
apply_entity_transform(self, transform);
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,11 @@ pub async fn open_path_with_phase(
|
|||
10000,
|
||||
);
|
||||
};
|
||||
let mut caches = crate::scene::build_derived_caches_with_progress(&doc, &cache_progress);
|
||||
let mut caches = crate::scene::build_derived_caches_with_progress(
|
||||
&doc,
|
||||
&cache_progress,
|
||||
path2.parent(),
|
||||
);
|
||||
caches.timings = crate::scene::OpenTimings {
|
||||
parse_ms,
|
||||
purge_ms,
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ pub fn parse_obj(src: &str, color: [f32; 4]) -> Option<MeshModel> {
|
|||
verts_low: Vec::new(),
|
||||
normals: norms,
|
||||
indices,
|
||||
triangle_material_handles: Vec::new(),
|
||||
triangle_colors: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
41
src/scene/cache/block_cache.rs
vendored
41
src/scene/cache/block_cache.rs
vendored
|
|
@ -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")
|
||||
|
|
|
|||
39
src/scene/cache/properties.rs
vendored
39
src/scene/cache/properties.rs
vendored
|
|
@ -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
|
|||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
®ion.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, ®ion.acis_data);
|
||||
attach_stored_silhouettes(&mut set, ®ion.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 ──────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
466
src/scene/mod.rs
466
src/scene/mod.rs
|
|
@ -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(_))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
447
src/scene/model/material_model.rs
Normal file
447
src/scene/model/material_model.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
278
src/scene/model/visual_style_model.rs
Normal file
278
src/scene/model/visual_style_model.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(¶ms),
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,51 @@ struct Uniforms {
|
|||
@group(0) @binding(0)
|
||||
var<uniform> u: Uniforms;
|
||||
|
||||
struct MaterialMaps {
|
||||
blends0: vec4<f32>,
|
||||
present0: vec4<u32>,
|
||||
blends1: vec4<f32>,
|
||||
present1: vec4<u32>,
|
||||
};
|
||||
|
||||
@group(1) @binding(0) var diffuse_map: texture_2d<f32>;
|
||||
@group(1) @binding(1) var specular_map: texture_2d<f32>;
|
||||
@group(1) @binding(2) var reflection_map: texture_2d<f32>;
|
||||
@group(1) @binding(3) var opacity_map: texture_2d<f32>;
|
||||
@group(1) @binding(4) var bump_map: texture_2d<f32>;
|
||||
@group(1) @binding(5) var refraction_map: texture_2d<f32>;
|
||||
@group(1) @binding(6) var normal_map: texture_2d<f32>;
|
||||
@group(1) @binding(7) var diffuse_sampler: sampler;
|
||||
@group(1) @binding(8) var specular_sampler: sampler;
|
||||
@group(1) @binding(9) var reflection_sampler: sampler;
|
||||
@group(1) @binding(10) var opacity_sampler: sampler;
|
||||
@group(1) @binding(11) var bump_sampler: sampler;
|
||||
@group(1) @binding(12) var refraction_sampler: sampler;
|
||||
@group(1) @binding(13) var normal_sampler: sampler;
|
||||
@group(1) @binding(14) var<uniform> maps: MaterialMaps;
|
||||
|
||||
struct VertexIn {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) color: vec4<f32>,
|
||||
@location(3) position_low: vec3<f32>,
|
||||
// gloss, reflectivity, self illumination, luminance
|
||||
@location(4) material: vec4<f32>,
|
||||
// specular RGB, refraction index
|
||||
@location(5) specular: vec4<f32>,
|
||||
@location(6) uv_diffuse: vec2<f32>,
|
||||
// ambient RGB, translucence
|
||||
@location(7) ambient: vec4<f32>,
|
||||
// normal strength, bump scale, reflectance scale, transmittance scale
|
||||
@location(8) advanced: vec4<f32>,
|
||||
// illumination model, channel flags, material mode, luminance mode
|
||||
@location(9) flags: vec4<u32>,
|
||||
@location(10) uv_specular: vec2<f32>,
|
||||
@location(11) uv_reflection: vec2<f32>,
|
||||
@location(12) uv_opacity: vec2<f32>,
|
||||
@location(13) uv_bump: vec2<f32>,
|
||||
@location(14) uv_refraction: vec2<f32>,
|
||||
@location(15) uv_normal: vec2<f32>,
|
||||
};
|
||||
|
||||
struct VertexOut {
|
||||
|
|
@ -39,6 +79,19 @@ struct VertexOut {
|
|||
@location(0) color: vec4<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) world_pos: vec3<f32>,
|
||||
@location(3) material: vec4<f32>,
|
||||
@location(4) specular: vec4<f32>,
|
||||
@location(5) eye_vec: vec3<f32>,
|
||||
@location(6) uv_diffuse: vec2<f32>,
|
||||
@location(7) ambient: vec4<f32>,
|
||||
@location(8) advanced: vec4<f32>,
|
||||
@location(9) @interpolate(flat) flags: vec4<u32>,
|
||||
@location(10) uv_specular: vec2<f32>,
|
||||
@location(11) uv_reflection: vec2<f32>,
|
||||
@location(12) uv_opacity: vec2<f32>,
|
||||
@location(13) uv_bump: vec2<f32>,
|
||||
@location(14) uv_refraction: vec2<f32>,
|
||||
@location(15) uv_normal: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
|
|
@ -48,7 +101,20 @@ fn vs_main(v: VertexIn) -> VertexOut {
|
|||
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
|
||||
out.color = v.color;
|
||||
out.normal = v.normal;
|
||||
out.world_pos = v.position;
|
||||
out.world_pos = rel;
|
||||
out.material = v.material;
|
||||
out.specular = v.specular;
|
||||
out.eye_vec = -rel;
|
||||
out.uv_diffuse = v.uv_diffuse;
|
||||
out.ambient = v.ambient;
|
||||
out.advanced = v.advanced;
|
||||
out.flags = v.flags;
|
||||
out.uv_specular = v.uv_specular;
|
||||
out.uv_reflection = v.uv_reflection;
|
||||
out.uv_opacity = v.uv_opacity;
|
||||
out.uv_bump = v.uv_bump;
|
||||
out.uv_refraction = v.uv_refraction;
|
||||
out.uv_normal = v.uv_normal;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +130,37 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
|||
n = normalize(in.normal);
|
||||
}
|
||||
|
||||
// Build a derivative tangent frame from the generated material UVs. This
|
||||
// works for planar/box/cylindrical/spherical AcDbMaterial projections and
|
||||
// does not require ACIS to carry explicit texture-coordinate vertices.
|
||||
let dp1 = dpdx(in.world_pos);
|
||||
let dp2 = dpdy(in.world_pos);
|
||||
let tangent_uv = select(in.uv_bump, in.uv_normal, maps.present1.z != 0u);
|
||||
let duv1 = dpdx(tangent_uv);
|
||||
let duv2 = dpdy(tangent_uv);
|
||||
let det = duv1.x * duv2.y - duv1.y * duv2.x;
|
||||
var tangent = normalize(dp1);
|
||||
var bitangent = normalize(cross(n, tangent));
|
||||
if (abs(det) > 1e-8) {
|
||||
tangent = normalize((dp1 * duv2.y - dp2 * duv1.y) / det);
|
||||
bitangent = normalize((-dp1 * duv2.x + dp2 * duv1.x) / det);
|
||||
}
|
||||
if (maps.present1.z != 0u) {
|
||||
let sampled = textureSample(normal_map, normal_sampler, in.uv_normal).xyz * 2.0 - 1.0;
|
||||
let mapped = normalize(
|
||||
tangent * sampled.x + bitangent * sampled.y + n * sampled.z
|
||||
);
|
||||
let strength = maps.blends1.z * max(in.advanced.x, 0.0);
|
||||
n = normalize(mix(n, mapped, clamp(strength, 0.0, 1.0)));
|
||||
} else if (maps.present1.x != 0u) {
|
||||
let height = dot(
|
||||
textureSample(bump_map, bump_sampler, in.uv_bump).rgb,
|
||||
vec3<f32>(0.2126, 0.7152, 0.0722),
|
||||
);
|
||||
let slope = tangent * dpdx(height) + bitangent * dpdy(height);
|
||||
n = normalize(n - slope * maps.blends1.x * max(in.advanced.y, 0.0) * 4.0);
|
||||
}
|
||||
|
||||
// Three-point-ish lighting (world space) plus ambient. Spread directions
|
||||
// keep every face — and the back faces seen through an open surface — lit
|
||||
// from at least one source, so the model never reads as a flat dark mass.
|
||||
|
|
@ -71,13 +168,69 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
|||
let l0 = normalize(vec3<f32>( 0.5, 0.8, 0.6)); // key (upper front)
|
||||
let l1 = normalize(vec3<f32>(-0.7, 0.3, 0.4)); // fill (left)
|
||||
let l2 = normalize(vec3<f32>( 0.2, -0.6, -0.8)); // back/under
|
||||
let ambient = 0.35;
|
||||
let diff = ambient
|
||||
+ 0.45 * abs(dot(n, l0))
|
||||
let diff = 0.45 * abs(dot(n, l0))
|
||||
+ 0.30 * abs(dot(n, l1))
|
||||
+ 0.25 * abs(dot(n, l2));
|
||||
let rgb = in.color.rgb * clamp(diff, 0.0, 1.0);
|
||||
return vec4<f32>(rgb, in.color.a);
|
||||
let view = normalize(in.eye_vec);
|
||||
let half_vec = normalize(l0 + view);
|
||||
let gloss_exp = mix(2.0, 128.0, clamp(in.material.x, 0.0, 1.0));
|
||||
let fresnel0 = pow(
|
||||
(max(in.specular.w, 1.0) - 1.0) / (max(in.specular.w, 1.0) + 1.0),
|
||||
2.0,
|
||||
);
|
||||
let fresnel = fresnel0
|
||||
+ (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0);
|
||||
let specular_strength = clamp(0.08 + in.material.y + fresnel, 0.0, 1.5);
|
||||
var specular_color = in.specular.rgb;
|
||||
if (maps.present0.y != 0u) {
|
||||
let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb;
|
||||
specular_color = mix(specular_color, texel, clamp(maps.blends0.y, 0.0, 1.0));
|
||||
}
|
||||
let specular = specular_color
|
||||
* pow(max(abs(dot(n, half_vec)), 0.0), gloss_exp)
|
||||
* specular_strength
|
||||
* max(in.advanced.z, 0.0);
|
||||
var albedo = in.color.rgb;
|
||||
if (maps.present0.x != 0u) {
|
||||
let texel = textureSample(diffuse_map, diffuse_sampler, in.uv_diffuse).rgb;
|
||||
albedo = mix(albedo, texel, clamp(maps.blends0.x, 0.0, 1.0));
|
||||
}
|
||||
let ambient_light = albedo * clamp(in.ambient.rgb, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
var lit = ambient_light + albedo * clamp(diff, 0.0, 1.0) + specular;
|
||||
if (maps.present0.z != 0u) {
|
||||
let reflected = textureSample(
|
||||
reflection_map,
|
||||
reflection_sampler,
|
||||
in.uv_reflection,
|
||||
).rgb;
|
||||
lit = mix(
|
||||
lit,
|
||||
reflected,
|
||||
clamp(maps.blends0.z * in.material.y * max(in.advanced.z, 0.0), 0.0, 1.0),
|
||||
);
|
||||
}
|
||||
if (maps.present1.y != 0u) {
|
||||
let transmitted = textureSample(
|
||||
refraction_map,
|
||||
refraction_sampler,
|
||||
in.uv_refraction,
|
||||
).rgb;
|
||||
lit = mix(
|
||||
lit,
|
||||
transmitted,
|
||||
clamp(maps.blends1.y * in.ambient.a * max(in.advanced.w, 0.0), 0.0, 1.0),
|
||||
);
|
||||
}
|
||||
let emission = clamp(in.material.z + in.material.w, 0.0, 1.0);
|
||||
let rgb = mix(lit, albedo, emission);
|
||||
var material_alpha = in.color.a;
|
||||
if (maps.present0.w != 0u) {
|
||||
let opacity = textureSample(opacity_map, opacity_sampler, in.uv_opacity).r;
|
||||
material_alpha *= mix(1.0, opacity, clamp(maps.blends0.w, 0.0, 1.0));
|
||||
}
|
||||
material_alpha *= 1.0 - clamp(in.ambient.a * max(in.advanced.w, 0.0), 0.0, 0.95);
|
||||
let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5);
|
||||
return vec4<f32>(rgb, alpha);
|
||||
}
|
||||
|
||||
// Edge fragment (LineList): flat entity colour, no lighting. Used for the
|
||||
|
|
|
|||
Loading…
Reference in a new issue