forked from 1b-agent/opencadstudio-upstream
feat(scene): integrate standard DWG data
This commit is contained in:
1 parent
731df4a725
commit
eb23715605
38 files changed
+8376
-618
No files matched your search
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user