fix(render): draw block-contained 3D faces

This commit is contained in:
Hakan Seven committed 2026-08-19 23:34:10 +03:00
1 parent c8840e2706
commit 8601fd8e4b
7 files changed
+90 -60

No files matched your search

+53 -1
View File
@@ -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<usize> = 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<RenderEntity> {
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,
})
}
}
+11 -1
View File
@@ -48,6 +48,7 @@ pub struct LocalWire {
pub tangent_geoms: Vec<TangentGeom>,
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<f32>,
) -> 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;
+3 -2
View File
@@ -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,
+13 -31
View File
@@ -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<Face3DVertex> = Vec::with_capacity(face3d_wires.len() * 6);
let mut verts_2d: Vec<Face3DVertex> = 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]),
});
}
}
}
+6 -18
View File
@@ -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<WireGpu> = 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 {
+3 -5
View File
@@ -127,11 +127,9 @@ fn handle_of(w: &WireModel) -> Option<Handle> {
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<u64>) -> 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<u64>) -> bool {
!w.points.is_empty() && w.fill_is_3d
}
/// True when appending a new entity at the tail could change the image, so the
+1 -2
View File
@@ -646,7 +646,6 @@ impl BlockWireGpu {
device: &wgpu::Device,
wires: &[&WireModel],
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
mesh_names: &rustc_hash::FxHashSet<&str>,
color_override: Option<[f32; 4]>,
const_bgl: &wgpu::BindGroupLayout,
) -> Vec<Self> {
@@ -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();