diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index be8cd4636..664ab8757 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -1,4 +1,6 @@ use acadrust::entities::{mesh::Mesh, polygon_mesh::PolygonMesh, Face3D, PolyfaceMesh}; +use cadkernel::geom2d::{triangulate, Tolerance}; +use cadkernel::space::{polygon, Plane, Vec3 as KernelVec3}; use glam::Vec3; use crate::t; @@ -151,12 +153,62 @@ fn v3f32(v: &acadrust::types::Vector3) -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] } +fn face3d_fill(corners: [[f64; 3]; 4]) -> Vec<[f64; 3]> { + let tolerance = Tolerance::default(); + let mut ring = Vec::with_capacity(4); + for corner in corners { + if ring.last().is_none_or(|previous| { + KernelVec3::from(*previous).distance(KernelVec3::from(corner)) + > tolerance.linear() + }) { + ring.push(corner); + } + } + if ring.len() > 1 + && KernelVec3::from(ring[0]).distance(KernelVec3::from(*ring.last().unwrap())) + <= tolerance.linear() + { + ring.pop(); + } + if ring.len() < 3 { + return Vec::new(); + } + + let Some(normal) = polygon::normal(&ring) else { + return Vec::new(); + }; + let axis = (KernelVec3::from(ring[1]) - KernelVec3::from(ring[0])).to_array(); + let Some(plane) = Plane::orthonormal(ring[0], axis, normal) else { + return Vec::new(); + }; + let projected: Vec<[f64; 2]> = ring + .iter() + .filter_map(|&point| plane.project(point)) + .collect(); + if projected.len() != ring.len() { + return Vec::new(); + } + let (points, triangles) = triangulate(&projected, &[]); + let source: Vec = points + .iter() + .filter_map(|point| projected.iter().position(|candidate| candidate == point)) + .collect(); + if source.len() != points.len() { + return Vec::new(); + } + triangles + .into_iter() + .flat_map(|triangle| triangle.map(|index| ring[source[index]])) + .collect() +} + impl RenderConvertible for Face3D { fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let p0 = v3(&self.first_corner); let p1 = v3(&self.second_corner); let p2 = v3(&self.third_corner); let p3 = v3(&self.fourth_corner); + let fill_tris = face3d_fill([p0, p1, p2, p3]); let p0f = v3f32(&self.first_corner); let p1f = v3f32(&self.second_corner); let p2f = v3f32(&self.third_corner); @@ -206,7 +258,7 @@ impl RenderConvertible for Face3D { ], tangent_geoms: vec![], key_vertices: vec![p0, p1, p2, p3], - fill_tris: vec![], + fill_tris, }) } } diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 14d57accb..d5c1d5f11 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -48,6 +48,7 @@ pub struct LocalWire { pub tangent_geoms: Vec, pub fill_tris: Vec<[f32; 3]>, pub fill_tris_low: Vec<[f32; 3]>, + pub fill_is_3d: bool, /// Preserves the planar SOLID classification through block expansion so /// it never gets merged with unrelated annotation fills of the same style. pub fill_is_2d_solid: bool, @@ -789,6 +790,7 @@ fn tessellate_sub_local( tangent_geoms: wire.tangent_geoms, fill_tris: wire.fill_tris, fill_tris_low: wire.fill_tris_low, + fill_is_3d: wire.fill_is_3d, fill_is_2d_solid: wire.fill_is_2d_solid, pick_tris: wire.pick_tris, pick_tris_low: wire.pick_tris_low, @@ -1359,6 +1361,7 @@ struct StyleKey { /// Part of the batch key so planar SOLID fills remain independently /// switchable after block geometry is merged by style. fill_is_2d_solid: bool, + fill_is_3d: bool, /// Bit-cast composed block-local depth for band wires (`0` = no override). /// Keeps bands of different in-block draw ranks in separate batches so /// each finalized WireModel carries one correct `depth_override`. @@ -1391,6 +1394,7 @@ struct BatchEntry { /// reconstructs `high + low`). Without it absolute f32 fills quantize to /// ~0.5 m and the greek-text rectangles shear. fill_tris_low: Vec<[f32; 3]>, + fill_is_3d: bool, fill_is_2d_solid: bool, /// Accumulated thickness-wall pick geometry, paired high/low like /// `fill_tris`. Pick-only — no GPU batch reads this. @@ -1455,6 +1459,7 @@ impl BatchEntry { plinegen: bool, _is_fill_only: bool, fill_is_2d_solid: bool, + fill_is_3d: bool, ) -> Self { // `is_fill_only` is part of the StyleKey hash so greek fills never // share a batch with regular wires (otherwise the finalized @@ -1473,6 +1478,7 @@ impl BatchEntry { aci, plinegen, fill_is_2d_solid, + fill_is_3d, min_x: f32::INFINITY, min_y: f32::INFINITY, max_x: f32::NEG_INFINITY, @@ -1515,7 +1521,7 @@ impl Batches { taper_widths: Vec::new(), world_width: b.world_width, depth_override: b.local_depth, - fill_is_3d: false, + fill_is_3d: b.fill_is_3d, fill_is_2d_solid: b.fill_is_2d_solid, render_instance: None, pick_tris: b.pick_tris, @@ -1566,6 +1572,7 @@ fn style_key( plinegen: bool, is_fill_only: bool, fill_is_2d_solid: bool, + fill_is_3d: bool, local_depth: Option, ) -> StyleKey { StyleKey { @@ -1595,6 +1602,7 @@ fn style_key( plinegen, is_fill_only, fill_is_2d_solid, + fill_is_3d, depth_bits: local_depth.map_or(0, f32::to_bits), } } @@ -2005,6 +2013,7 @@ fn emit_wire( lw.plinegen, lw.is_fill_only, lw.fill_is_2d_solid, + lw.fill_is_3d, local_depth, ); @@ -2031,6 +2040,7 @@ fn emit_wire( lw.plinegen, lw.is_fill_only, lw.fill_is_2d_solid, + lw.fill_is_3d, ) }); entry.local_depth = local_depth; diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 7dec844ce..64f8dd8a5 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1156,7 +1156,8 @@ pub fn tessellate( // its face fill into the 2-D buffer, so it drew in wireframe too. let fill_is_3d = matches!( entity, - EntityType::PolyfaceMesh(_) + EntityType::Face3D(_) + | EntityType::PolyfaceMesh(_) | EntityType::PolygonMesh(_) | EntityType::Mesh(_) ); @@ -1213,7 +1214,7 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: polyline_band_width(entity), depth_override: None, - fill_is_3d: false, + fill_is_3d, fill_is_2d_solid: false, render_instance: None, pick_tris, diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index 068f7b52e..afb58bc6d 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -133,8 +133,7 @@ pub struct Face3DGpu { impl Face3DGpu { /// Build a batched GPU buffer from Face3D wire models and mesh fill_tris. /// - /// - `face3d_wires`: Face3D entities — `key_vertices` holds 4 quad corners; - /// emits 2 triangles per face into the 3D buffer. + /// - `face3d_wires`: kernel-triangulated Face3D entities. /// - `all_wires`: all entity wires — `fill_tris` holds pre-triangulated /// fill data. Fills with a non-empty `fill_tris_low` residual (real 3-D /// surfaces — PolyfaceMesh / PolygonMesh) feed the 3D buffer at their @@ -157,43 +156,26 @@ impl Face3DGpu { let mut verts_3d: Vec = Vec::with_capacity(face3d_wires.len() * 6); let mut verts_2d: Vec = Vec::new(); - // Face3D quads (4 key_vertices → 2 triangles) — only when 3D - // fills are wanted. + // Face3D kernel triangles — only when 3D fills are wanted. if keep_3d_mesh_fills { for wire in face3d_wires { - if wire.key_vertices.len() < 4 { + if wire.fill_tris.is_empty() { continue; } let [r, g, b, a] = wire.color; let fill_color = [r * 0.45, g * 0.45, b * 0.45, a]; - let p = &wire.key_vertices; - // key_vertices are f64 (offset-relative); split into the - // double-single (high, low) pair the face3d shader expects. - let v = |i: usize| { - let [x, y, z] = p[i]; - let h = [x as f32, y as f32, z as f32]; - Face3DVertex { - position: h, + for (index, &position) in wire.fill_tris.iter().enumerate() { + verts_3d.push(Face3DVertex { + position, color: fill_color, - // A 3DFACE is genuine 3D geometry: keep its real depth - // (no draw-order bias), matching the PolyfaceMesh / - // PolygonMesh path below. A non-zero draw-order rank - // here yanked 3DFACEs toward the camera so they drew in - // front of solids (which carry no such bias). draw_depth: 0.0, - position_low: [ - (x - h[0] as f64) as f32, - (y - h[1] as f64) as f32, - (z - h[2] as f64) as f32, - ], - } - }; - verts_3d.push(v(0)); - verts_3d.push(v(1)); - verts_3d.push(v(2)); - verts_3d.push(v(0)); - verts_3d.push(v(2)); - verts_3d.push(v(3)); + position_low: wire + .fill_tris_low + .get(index) + .copied() + .unwrap_or([0.0; 3]), + }); + } } } diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index a59fc1a60..1a9cd2f6a 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -2359,19 +2359,7 @@ impl Pipeline { // and alpha blending both depend on it. Scissor and mesh-edge stay // grouping keys because the draw loop sets one scissor per batch and // skips whole mesh-edge batches in shaded modes. - // A 3D mesh entity (PolyfaceMesh / PolygonMesh) emits its face fill and - // its outline edges as *separate* WireModels sharing the entity handle - // (`name`): the fill carries `fill_tris` + a non-empty `fill_tris_low` - // (real 3D depth, same test face3d uses); the edge carries `points`. - // Flag the edge wire as `is_3d_mesh_edge` so the draw loop can hide it in - // clean-shaded modes and draw it black in filled-with-edges modes. - let mesh_names: rustc_hash::FxHashSet<&str> = wires - .iter() - .filter(|w| !w.fill_tris.is_empty() && !w.fill_tris_low.is_empty()) - .map(|w| w.name.as_str()) - .collect(); - let is_mesh_edge = - |w: &WireModel| !w.points.is_empty() && mesh_names.contains(w.name.as_str()); + let is_mesh_edge = |w: &WireModel| !w.points.is_empty() && w.fill_is_3d; let mut batches: Vec = Vec::new(); let mut block_wires: Vec<&WireModel> = Vec::new(); let mut i = 0; @@ -2403,7 +2391,6 @@ impl Pipeline { device, &block_wires, depth_map, - &mesh_names, None, &self.block_wire_const_bgl, ); @@ -2516,12 +2503,10 @@ impl Pipeline { self.wire_const_bgl.as_ref(), )); self.gpu_selected_wires = gpu; - let mesh_names = rustc_hash::FxHashSet::default(); let mut block_gpu = BlockWireGpu::from_wires( device, &selected_blocks, depth_map, - &mesh_names, Some(WireModel::SELECTED), &self.block_wire_const_bgl, ); @@ -2529,7 +2514,6 @@ impl Pipeline { device, &hover_blocks, depth_map, - &mesh_names, Some(WireModel::HOVER), &self.block_wire_const_bgl, )); @@ -4046,7 +4030,11 @@ impl Pipeline { // pipeline in HiddenLine so wires hidden behind them disappear. // 2D fills (text greek, MultiLeader bg) always draw with colour. if let Some(ref fill) = self.gpu_face3d_fill { - if !fill.chunks_3d.is_empty() || !fill.chunks_2d.is_empty() { + if !fill.chunks_3d.is_empty() + || !fill.chunks_2d.is_empty() + || !fill.block_chunks_3d.is_empty() + || !fill.block_chunks_2d.is_empty() + { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("face3d.render_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index 84ab54bd6..f7ae562bc 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -127,11 +127,9 @@ fn handle_of(w: &WireModel) -> Option { crate::scene::Scene::handle_from_wire_name(&w.name) } -/// True when `w`'s edge segments belong to a mesh/solid whose fill is drawn in a -/// separate pass — such wires need the draw loop's mesh-edge treatment, so they -/// go in their own arena. `mesh_names` are the entities that emit a fill. -pub fn is_mesh_edge(w: &WireModel, mesh_names: &rustc_hash::FxHashSet) -> bool { - !w.points.is_empty() && handle_of(w).map_or(false, |h| mesh_names.contains(&h.value())) +/// True when `w` needs the shaded-mode edge treatment. +pub fn is_mesh_edge(w: &WireModel, _mesh_names: &rustc_hash::FxHashSet) -> bool { + !w.points.is_empty() && w.fill_is_3d } /// True when appending a new entity at the tail could change the image, so the diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 49212ca48..f880ee7d5 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -646,7 +646,6 @@ impl BlockWireGpu { device: &wgpu::Device, wires: &[&WireModel], depth_map: &rustc_hash::FxHashMap, - mesh_names: &rustc_hash::FxHashSet<&str>, color_override: Option<[f32; 4]>, const_bgl: &wgpu::BindGroupLayout, ) -> Vec { @@ -662,7 +661,7 @@ impl BlockWireGpu { if wire.points.len() < 2 { continue; } - let mesh_edge = mesh_names.contains(wire.name.as_str()); + let mesh_edge = color_override.is_none() && wire.fill_is_3d; let key = (instance.source_id, mesh_edge); let slot = *slots.entry(key).or_insert_with(|| { let slot = groups.len();