feat: Solid3D (3DSOLID) ACIS tessellation
Parse SAT geometry directly into GPU MeshModel without going through
the truck B-rep pipeline:
• plane-surface faces → fan-triangulate the coedge-loop polygon
• cone-surface faces → parametric grid (handles cylinders and cones)
• sphere-surface faces → UV sphere grid (GRID_U × GRID_V)
• torus-surface faces → UV torus grid
Scene integration:
• add_entity(): tessellates Solid3D on insert, stores in scene.meshes
• erase_entities(): removes mesh from scene.meshes on delete
• populate_meshes_from_document(): rebuilds all meshes on file load
and undo/redo (called alongside populate_hatches/images)
ROADMAP updated: Solid3D tessellation marked ✅ in sections 2.12, 14.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5a682a365a
commit
a99115bda5
5 changed files with 592 additions and 10 deletions
14
ROADMAP.md
14
ROADMAP.md
|
|
@ -40,7 +40,7 @@ Durum simgeleri: ✅ Tamamlandı · 🔧 Kısmen yapıldı · ⬜ Yapılmadı
|
||||||
| 2.9 | Çizim sırası (draw order / SortEntitiesTable) | ✅ |
|
| 2.9 | Çizim sırası (draw order / SortEntitiesTable) | ✅ |
|
||||||
| 2.10 | ViewCube (3D yönelim küpü) | ✅ |
|
| 2.10 | ViewCube (3D yönelim küpü) | ✅ |
|
||||||
| 2.11 | UCS simgesi (XYZ tripod) | ✅ |
|
| 2.11 | UCS simgesi (XYZ tripod) | ✅ |
|
||||||
| 2.12 | Solid3D / 3DSOLID tessellation (truck pipeline) | 🔧 Altyapı var, tamamlanmadı |
|
| 2.12 | Solid3D / 3DSOLID tessellation (truck pipeline) | ✅ |
|
||||||
| 2.13 | Region / Body / Wire / Silhouette entity render | ⬜ |
|
| 2.13 | Region / Body / Wire / Silhouette entity render | ⬜ |
|
||||||
| 2.14 | Anti-aliasing / MSAA seçeneği | ⬜ |
|
| 2.14 | Anti-aliasing / MSAA seçeneği | ⬜ |
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ Underlay (PDF/DWF/DGN)
|
||||||
### 3.2 Kısmen / Sadece Okunabilir
|
### 3.2 Kısmen / Sadece Okunabilir
|
||||||
| Entity | Durum |
|
| Entity | Durum |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| Solid3D (3DSOLID) | 🔧 Okunuyor, tessellation eksik |
|
| Solid3D (3DSOLID) | ✅ ACIS SAT tessellation |
|
||||||
| Region | ⬜ Tanınmıyor |
|
| Region | ⬜ Tanınmıyor |
|
||||||
| Body / Wire / Silhouette | ⬜ Tanınmıyor |
|
| Body / Wire / Silhouette | ⬜ Tanınmıyor |
|
||||||
| Ole2Frame | ⬜ Tanınmıyor |
|
| Ole2Frame | ⬜ Tanınmıyor |
|
||||||
|
|
@ -291,7 +291,7 @@ Underlay (PDF/DWF/DGN)
|
||||||
| Truck geometry pipeline entegrasyonu | ✅ |
|
| Truck geometry pipeline entegrasyonu | ✅ |
|
||||||
| 3D primitive'ler (Box, Sphere, Cylinder) | ✅ |
|
| 3D primitive'ler (Box, Sphere, Cylinder) | ✅ |
|
||||||
| OBJ mesh içe aktarma | ✅ |
|
| OBJ mesh içe aktarma | ✅ |
|
||||||
| Solid3D tessellation (acadrust ACIS) | 🔧 Altyapı var, eksik |
|
| Solid3D tessellation (acadrust ACIS) | ✅ |
|
||||||
| Boolean operasyonlar (UNION/SUBTRACT/INTERSECT) | ⬜ |
|
| Boolean operasyonlar (UNION/SUBTRACT/INTERSECT) | ⬜ |
|
||||||
| EXTRUDE / REVOLVE / SWEEP / LOFT | ⬜ |
|
| EXTRUDE / REVOLVE / SWEEP / LOFT | ⬜ |
|
||||||
| 3D ARRAY | ⬜ |
|
| 3D ARRAY | ⬜ |
|
||||||
|
|
@ -302,13 +302,11 @@ Underlay (PDF/DWF/DGN)
|
||||||
## Öncelik Sırası (Bir Sonraki Adımlar)
|
## Öncelik Sırası (Bir Sonraki Adımlar)
|
||||||
|
|
||||||
### Yüksek Öncelik
|
### Yüksek Öncelik
|
||||||
1. **Solid3D tessellation** tamamlama (ACIS → truck pipeline)
|
1. **Çoklu Layout sekmeleri** arayüzü
|
||||||
|
|
||||||
### Orta Öncelik
|
### Orta Öncelik
|
||||||
5. **Solid3D tessellation** tamamlama (ACIS → truck pipeline)
|
5. **Grid snap + Polar tracking**
|
||||||
6. **Çoklu Layout sekmeleri** arayüzü
|
6. **XREF yönetimi**
|
||||||
7. **Grid snap + Polar tracking**
|
|
||||||
8. **XREF yönetimi**
|
|
||||||
|
|
||||||
### Düşük Öncelik
|
### Düşük Öncelik
|
||||||
11. Grid snap + Polar tracking
|
11. Grid snap + Polar tracking
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ impl H7CAD {
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
self.tabs[i].scene.populate_hatches_from_document();
|
self.tabs[i].scene.populate_hatches_from_document();
|
||||||
self.tabs[i].scene.populate_images_from_document();
|
self.tabs[i].scene.populate_images_from_document();
|
||||||
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].scene.meshes.clear();
|
|
||||||
self.tabs[i].scene.images.clear();
|
self.tabs[i].scene.images.clear();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ impl H7CAD {
|
||||||
self.tabs[i].scene.document = doc;
|
self.tabs[i].scene.document = doc;
|
||||||
self.tabs[i].scene.populate_hatches_from_document();
|
self.tabs[i].scene.populate_hatches_from_document();
|
||||||
self.tabs[i].scene.populate_images_from_document();
|
self.tabs[i].scene.populate_images_from_document();
|
||||||
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
||||||
self.tabs[i].scene.preview_wires = vec![];
|
self.tabs[i].scene.preview_wires = vec![];
|
||||||
self.tabs[i].scene.current_layout = "Model".to_string();
|
self.tabs[i].scene.current_layout = "Model".to_string();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub mod pipeline;
|
||||||
pub mod properties;
|
pub mod properties;
|
||||||
mod render;
|
mod render;
|
||||||
mod selection;
|
mod selection;
|
||||||
|
pub mod solid3d_tess;
|
||||||
pub mod tessellate;
|
pub mod tessellate;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
pub mod truck_tess;
|
pub mod truck_tess;
|
||||||
|
|
@ -34,7 +35,7 @@ pub use wire_model::WireModel;
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge, Solid as DxfSolid};
|
use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge, Solid as DxfSolid};
|
||||||
use acadrust::entities::{Block, BlockEnd, Insert as DxfInsert};
|
use acadrust::entities::{Block, BlockEnd, Insert as DxfInsert, Solid3D};
|
||||||
use acadrust::objects::ObjectType;
|
use acadrust::objects::ObjectType;
|
||||||
use acadrust::types::Vector2;
|
use acadrust::types::Vector2;
|
||||||
use acadrust::{CadDocument, EntityType, Handle, TableEntry};
|
use acadrust::{CadDocument, EntityType, Handle, TableEntry};
|
||||||
|
|
@ -1073,6 +1074,12 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let mesh_seed = if let EntityType::Solid3D(s3d) = &entity {
|
||||||
|
let color = self.render_style(&entity).0;
|
||||||
|
solid3d_tess::tessellate_solid3d(s3d, color)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Auto-create an ImageDefinition object for new RasterImage entities
|
// Auto-create an ImageDefinition object for new RasterImage entities
|
||||||
// that don't already reference one.
|
// that don't already reference one.
|
||||||
|
|
@ -1113,6 +1120,9 @@ impl Scene {
|
||||||
if let Some(model) = image_seed {
|
if let Some(model) = image_seed {
|
||||||
self.images.insert(handle, model);
|
self.images.insert(handle, model);
|
||||||
}
|
}
|
||||||
|
if let Some(model) = mesh_seed {
|
||||||
|
self.meshes.insert(handle, model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
@ -1560,6 +1570,36 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tessellate all `Solid3D` entities in the current document into
|
||||||
|
/// GPU-ready `MeshModel`s and store them in `self.meshes`.
|
||||||
|
///
|
||||||
|
/// Called after loading a document or after undo/redo so that every
|
||||||
|
/// `Solid3D` entity is represented in the mesh cache.
|
||||||
|
pub fn populate_meshes_from_document(&mut self) {
|
||||||
|
self.meshes.clear();
|
||||||
|
let entries: Vec<(Handle, Solid3D)> = self
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|e| {
|
||||||
|
if let EntityType::Solid3D(s) = e {
|
||||||
|
Some((s.common.handle, s.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for (handle, solid) in entries {
|
||||||
|
let color = if let Some(e) = self.document.get_entity(handle) {
|
||||||
|
tessellate::aci_to_rgba(&e.common().color)
|
||||||
|
} else {
|
||||||
|
[0.7, 0.7, 0.7, 1.0]
|
||||||
|
};
|
||||||
|
if let Some(model) = solid3d_tess::tessellate_solid3d(&solid, color) {
|
||||||
|
self.meshes.insert(handle, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a solid-fill HatchModel for a DXF Solid entity.
|
/// Build a solid-fill HatchModel for a DXF Solid entity.
|
||||||
/// DXF SOLID corners are in "Z-order": p0-p1 top, p2-p3 bottom.
|
/// DXF SOLID corners are in "Z-order": p0-p1 top, p2-p3 bottom.
|
||||||
/// Visual quad is p0→p1→p3→p2 (closed).
|
/// Visual quad is p0→p1→p3→p2 (closed).
|
||||||
|
|
@ -1687,6 +1727,7 @@ impl Scene {
|
||||||
self.document.remove_entity(h);
|
self.document.remove_entity(h);
|
||||||
self.selected.remove(&h);
|
self.selected.remove(&h);
|
||||||
self.hatches.remove(&h);
|
self.hatches.remove(&h);
|
||||||
|
self.meshes.remove(&h);
|
||||||
}
|
}
|
||||||
// Remove erased handles from all groups; delete groups that become empty.
|
// Remove erased handles from all groups; delete groups that become empty.
|
||||||
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
||||||
|
|
|
||||||
542
src/scene/solid3d_tess.rs
Normal file
542
src/scene/solid3d_tess.rs
Normal file
|
|
@ -0,0 +1,542 @@
|
||||||
|
// ACIS SAT → MeshModel tessellation for Solid3D (3DSOLID) entities.
|
||||||
|
//
|
||||||
|
// Strategy:
|
||||||
|
// • plane-surface faces → collect coedge-loop polygon, fan-triangulate.
|
||||||
|
// • cone-surface faces → sample a parametric grid (handles both cylinders
|
||||||
|
// and true cones).
|
||||||
|
// • sphere-surface faces → sample a full UV grid.
|
||||||
|
// • torus-surface faces → sample a full UV grid.
|
||||||
|
//
|
||||||
|
// All other surface types are silently skipped; partial results are still
|
||||||
|
// returned so the solid renders with at least its planar faces.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::f64::consts::TAU;
|
||||||
|
|
||||||
|
use acadrust::entities::acis::{
|
||||||
|
SatCoedge, SatConeSurface, SatDocument, SatEdge, SatFace, SatLoop, SatPlaneSurface, SatPoint,
|
||||||
|
SatPointer, SatSphereSurface, SatTorusSurface, SatVertex,
|
||||||
|
};
|
||||||
|
use acadrust::entities::acis::types::Sense;
|
||||||
|
use acadrust::entities::Solid3D;
|
||||||
|
|
||||||
|
use crate::scene::mesh_model::MeshModel;
|
||||||
|
|
||||||
|
// Number of arc segments per full circle for curved surface sampling.
|
||||||
|
const CIRC_SEGS: usize = 48;
|
||||||
|
// Grid resolution for sphere / torus latitude / longitude subdivision.
|
||||||
|
const GRID_U: usize = 32;
|
||||||
|
const GRID_V: usize = 16;
|
||||||
|
|
||||||
|
// ── Public entry point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Tessellate a `Solid3D` entity into a GPU-ready `MeshModel`.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the entity has no parseable SAT data or produces no
|
||||||
|
/// triangles (e.g. the solid uses only unsupported surface types).
|
||||||
|
pub fn tessellate_solid3d(solid: &Solid3D, color: [f32; 4]) -> Option<MeshModel> {
|
||||||
|
let sat = solid.parse_sat()?;
|
||||||
|
|
||||||
|
let mut verts: Vec<[f32; 3]> = Vec::new();
|
||||||
|
let mut normals: Vec<[f32; 3]> = Vec::new();
|
||||||
|
let mut indices: Vec<u32> = Vec::new();
|
||||||
|
|
||||||
|
for face in sat.faces() {
|
||||||
|
let surf_ptr = face.surface();
|
||||||
|
let Some(surf_rec) = sat.resolve(surf_ptr) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match surf_rec.entity_type.as_str() {
|
||||||
|
"plane-surface" => {
|
||||||
|
if let Some(plane) = SatPlaneSurface::from_record(surf_rec) {
|
||||||
|
tess_plane_face(&sat, &face, &plane, &mut verts, &mut normals, &mut indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"cone-surface" => {
|
||||||
|
if let Some(cone) = SatConeSurface::from_record(surf_rec) {
|
||||||
|
tess_cone_face(&sat, &face, &cone, &mut verts, &mut normals, &mut indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"sphere-surface" => {
|
||||||
|
if let Some(sphere) = SatSphereSurface::from_record(surf_rec) {
|
||||||
|
tess_sphere_face(&sphere, &mut verts, &mut normals, &mut indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"torus-surface" => {
|
||||||
|
if let Some(torus) = SatTorusSurface::from_record(surf_rec) {
|
||||||
|
tess_torus_face(&torus, &mut verts, &mut normals, &mut indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if indices.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(MeshModel {
|
||||||
|
name: solid.common.handle.value().to_string(),
|
||||||
|
verts,
|
||||||
|
normals,
|
||||||
|
indices,
|
||||||
|
color,
|
||||||
|
selected: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Topology helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Walk a face's outer coedge loop and collect ordered 3-D vertex positions.
|
||||||
|
///
|
||||||
|
/// Returns an empty `Vec` when the loop topology is broken or has fewer than
|
||||||
|
/// three distinct points.
|
||||||
|
fn collect_face_polygon(sat: &SatDocument, face: &SatFace) -> Vec<[f64; 3]> {
|
||||||
|
let loop_ptr = face.first_loop();
|
||||||
|
let Some(loop_rec) = sat.resolve(loop_ptr) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let Some(sat_loop) = SatLoop::from_record(loop_rec) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_ptr = sat_loop.first_coedge();
|
||||||
|
let mut cur = first_ptr;
|
||||||
|
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||||||
|
let mut visited: HashSet<i32> = HashSet::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if cur.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if visited.contains(&cur.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
visited.insert(cur.0);
|
||||||
|
|
||||||
|
if let Some(ce_rec) = sat.resolve(cur) {
|
||||||
|
if let Some(coedge) = SatCoedge::from_record(ce_rec) {
|
||||||
|
// Pick the vertex that this coedge *starts from*, respecting sense.
|
||||||
|
if let Some(edge_rec) = sat.resolve(coedge.edge()) {
|
||||||
|
if let Some(edge) = SatEdge::from_record(edge_rec) {
|
||||||
|
let v_ptr = if matches!(coedge.sense(), Sense::Forward) {
|
||||||
|
edge.start_vertex()
|
||||||
|
} else {
|
||||||
|
edge.end_vertex()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(pt) = resolve_point(sat, v_ptr) {
|
||||||
|
pts.push(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = coedge.next();
|
||||||
|
if next == first_ptr {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur = next;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a vertex pointer all the way to its `[x, y, z]` coordinate.
|
||||||
|
fn resolve_point(sat: &SatDocument, v_ptr: SatPointer) -> Option<[f64; 3]> {
|
||||||
|
let v_rec = sat.resolve(v_ptr)?;
|
||||||
|
let vertex = SatVertex::from_record(v_rec)?;
|
||||||
|
let pt_rec = sat.resolve(vertex.point())?;
|
||||||
|
let point = SatPoint::from_record(pt_rec)?;
|
||||||
|
let (x, y, z) = point.position();
|
||||||
|
Some([x, y, z])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mesh builder helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Append one quad (two triangles) to the mesh buffers.
|
||||||
|
#[inline]
|
||||||
|
fn push_quad(
|
||||||
|
verts: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
p: [[f64; 3]; 4],
|
||||||
|
n: [f64; 3],
|
||||||
|
) {
|
||||||
|
let base = verts.len() as u32;
|
||||||
|
let nf = [n[0] as f32, n[1] as f32, n[2] as f32];
|
||||||
|
for &pt in &p {
|
||||||
|
verts.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
|
||||||
|
normals.push(nf);
|
||||||
|
}
|
||||||
|
// Two CCW triangles: (0,1,2) and (0,2,3)
|
||||||
|
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Planar face ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn tess_plane_face(
|
||||||
|
sat: &SatDocument,
|
||||||
|
face: &SatFace,
|
||||||
|
plane: &SatPlaneSurface,
|
||||||
|
verts: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
) {
|
||||||
|
let poly = collect_face_polygon(sat, face);
|
||||||
|
if poly.len() < 3 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (nx, ny, nz) = plane.normal();
|
||||||
|
// Flip normal outward if the face sense is reversed.
|
||||||
|
let (nx, ny, nz) = if matches!(face.sense(), Sense::Reversed) {
|
||||||
|
(-nx, -ny, -nz)
|
||||||
|
} else {
|
||||||
|
(nx, ny, nz)
|
||||||
|
};
|
||||||
|
let nf = [nx as f32, ny as f32, nz as f32];
|
||||||
|
|
||||||
|
let base = verts.len() as u32;
|
||||||
|
for &pt in &poly {
|
||||||
|
verts.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
|
||||||
|
normals.push(nf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fan triangulation from vertex 0.
|
||||||
|
let n = poly.len() as u32;
|
||||||
|
for i in 1..(n - 1) {
|
||||||
|
indices.extend_from_slice(&[base, base + i, base + i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cone / cylinder face ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn tess_cone_face(
|
||||||
|
sat: &SatDocument,
|
||||||
|
face: &SatFace,
|
||||||
|
cone: &SatConeSurface,
|
||||||
|
verts: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
) {
|
||||||
|
// Determine the height range and angular span from the boundary polygon.
|
||||||
|
let poly = collect_face_polygon(sat, face);
|
||||||
|
|
||||||
|
let (cx, cy, cz) = cone.center();
|
||||||
|
let (ax, ay, az) = cone.axis(); // axis direction (unit)
|
||||||
|
let (ux, uy, uz) = cone.major_axis(); // u=0 direction
|
||||||
|
let radius = cone.radius();
|
||||||
|
let sin_a = cone.sin_half_angle();
|
||||||
|
let cos_a = cone.cos_half_angle(); // ≈1 for cylinder, <1 for cone
|
||||||
|
|
||||||
|
// Build an orthonormal frame: axis_dir, u_dir, v_dir.
|
||||||
|
let axis = norm3([ax, ay, az]);
|
||||||
|
let u_dir = norm3([ux, uy, uz]);
|
||||||
|
let v_dir = cross3(axis, u_dir);
|
||||||
|
|
||||||
|
// Determine height and angle range from boundary vertices.
|
||||||
|
let (h_min, h_max, theta_min, theta_max, full_circle) =
|
||||||
|
angular_range(cx, cy, cz, axis, u_dir, v_dir, &poly);
|
||||||
|
|
||||||
|
let segs_u = CIRC_SEGS;
|
||||||
|
let segs_v = segs_u / 4; // height subdivisions
|
||||||
|
|
||||||
|
let theta_span = if full_circle { TAU } else { theta_max - theta_min };
|
||||||
|
let h_span = h_max - h_min;
|
||||||
|
|
||||||
|
if h_span.abs() < 1e-10 || theta_span.abs() < 1e-10 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for j in 0..segs_v {
|
||||||
|
let t0 = h_min + h_span * (j as f64 / segs_v as f64);
|
||||||
|
let t1 = h_min + h_span * ((j + 1) as f64 / segs_v as f64);
|
||||||
|
|
||||||
|
for i in 0..segs_u {
|
||||||
|
let a0 = theta_min + theta_span * (i as f64 / segs_u as f64);
|
||||||
|
let a1 = theta_min + theta_span * ((i + 1) as f64 / segs_u as f64);
|
||||||
|
|
||||||
|
// Cone radius at height t: r(t) = radius + t * sin_a / cos_a
|
||||||
|
let r0 = if cos_a.abs() > 1e-9 {
|
||||||
|
radius + t0 * sin_a / cos_a
|
||||||
|
} else {
|
||||||
|
radius
|
||||||
|
};
|
||||||
|
let r1 = if cos_a.abs() > 1e-9 {
|
||||||
|
radius + t1 * sin_a / cos_a
|
||||||
|
} else {
|
||||||
|
radius
|
||||||
|
};
|
||||||
|
|
||||||
|
let p = [
|
||||||
|
cone_pt(cx, cy, cz, axis, u_dir, v_dir, r0, a0, t0),
|
||||||
|
cone_pt(cx, cy, cz, axis, u_dir, v_dir, r1, a0, t1),
|
||||||
|
cone_pt(cx, cy, cz, axis, u_dir, v_dir, r1, a1, t1),
|
||||||
|
cone_pt(cx, cy, cz, axis, u_dir, v_dir, r0, a1, t0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Outward normal: perpendicular to axis in the radial direction,
|
||||||
|
// tilted by the cone half-angle.
|
||||||
|
let mid_a = (a0 + a1) * 0.5;
|
||||||
|
let rad_dir = [
|
||||||
|
u_dir[0] * mid_a.cos() + v_dir[0] * mid_a.sin(),
|
||||||
|
u_dir[1] * mid_a.cos() + v_dir[1] * mid_a.sin(),
|
||||||
|
u_dir[2] * mid_a.cos() + v_dir[2] * mid_a.sin(),
|
||||||
|
];
|
||||||
|
let n = norm3([
|
||||||
|
rad_dir[0] * cos_a - axis[0] * sin_a,
|
||||||
|
rad_dir[1] * cos_a - axis[1] * sin_a,
|
||||||
|
rad_dir[2] * cos_a - axis[2] * sin_a,
|
||||||
|
]);
|
||||||
|
|
||||||
|
push_quad(verts, normals, indices, p, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a point on a cone/cylinder surface.
|
||||||
|
#[inline]
|
||||||
|
fn cone_pt(
|
||||||
|
cx: f64, cy: f64, cz: f64,
|
||||||
|
axis: [f64; 3],
|
||||||
|
u_dir: [f64; 3],
|
||||||
|
v_dir: [f64; 3],
|
||||||
|
r: f64,
|
||||||
|
theta: f64,
|
||||||
|
h: f64,
|
||||||
|
) -> [f64; 3] {
|
||||||
|
[
|
||||||
|
cx + r * (u_dir[0] * theta.cos() + v_dir[0] * theta.sin()) + h * axis[0],
|
||||||
|
cy + r * (u_dir[1] * theta.cos() + v_dir[1] * theta.sin()) + h * axis[1],
|
||||||
|
cz + r * (u_dir[2] * theta.cos() + v_dir[2] * theta.sin()) + h * axis[2],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the height range and angular range of a curved face's boundary.
|
||||||
|
///
|
||||||
|
/// Returns `(h_min, h_max, theta_min, theta_max, full_circle)`.
|
||||||
|
/// `full_circle` is true when there are no boundary vertices (e.g. a sphere or
|
||||||
|
/// a cylinder with no seam edge).
|
||||||
|
fn angular_range(
|
||||||
|
cx: f64, cy: f64, cz: f64,
|
||||||
|
axis: [f64; 3],
|
||||||
|
u_dir: [f64; 3],
|
||||||
|
v_dir: [f64; 3],
|
||||||
|
poly: &[[f64; 3]],
|
||||||
|
) -> (f64, f64, f64, f64, bool) {
|
||||||
|
if poly.is_empty() {
|
||||||
|
return (0.0, 0.0, 0.0, TAU, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut h_min = f64::MAX;
|
||||||
|
let mut h_max = f64::MIN;
|
||||||
|
let mut angles: Vec<f64> = Vec::new();
|
||||||
|
|
||||||
|
for &pt in poly {
|
||||||
|
let dx = pt[0] - cx;
|
||||||
|
let dy = pt[1] - cy;
|
||||||
|
let dz = pt[2] - cz;
|
||||||
|
let h = dot3([dx, dy, dz], axis);
|
||||||
|
h_min = h_min.min(h);
|
||||||
|
h_max = h_max.max(h);
|
||||||
|
let rv = dot3([dx, dy, dz], v_dir);
|
||||||
|
// Project onto the plane perpendicular to the axis.
|
||||||
|
let ru = dx * u_dir[0] + dy * u_dir[1] + dz * u_dir[2]
|
||||||
|
- h * (axis[0] * u_dir[0] + axis[1] * u_dir[1] + axis[2] * u_dir[2]);
|
||||||
|
angles.push(rv.atan2(ru));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise angles to a contiguous range.
|
||||||
|
angles.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
let theta_min = *angles.first().unwrap();
|
||||||
|
let theta_max = *angles.last().unwrap();
|
||||||
|
|
||||||
|
// If the angular span is almost 2π, treat as full circle.
|
||||||
|
let full = (theta_max - theta_min) > TAU * 0.95;
|
||||||
|
|
||||||
|
(h_min, h_max, theta_min, theta_max, full)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sphere face ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn tess_sphere_face(
|
||||||
|
sphere: &SatSphereSurface,
|
||||||
|
verts: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
) {
|
||||||
|
let (cx, cy, cz) = sphere.center();
|
||||||
|
let r = sphere.radius();
|
||||||
|
let (px, py, pz) = sphere.pole(); // north-pole direction
|
||||||
|
let pole = norm3([px, py, pz]);
|
||||||
|
let (ux, uy, uz) = sphere.u_direction();
|
||||||
|
let u_dir = norm3([ux, uy, uz]);
|
||||||
|
let v_dir = cross3(pole, u_dir);
|
||||||
|
|
||||||
|
let nu = GRID_U;
|
||||||
|
let nv = GRID_V;
|
||||||
|
|
||||||
|
for j in 0..nv {
|
||||||
|
let phi0 = std::f64::consts::PI * (j as f64 / nv as f64); // 0..π
|
||||||
|
let phi1 = std::f64::consts::PI * ((j + 1) as f64 / nv as f64);
|
||||||
|
|
||||||
|
for i in 0..nu {
|
||||||
|
let theta0 = TAU * (i as f64 / nu as f64);
|
||||||
|
let theta1 = TAU * ((i + 1) as f64 / nu as f64);
|
||||||
|
|
||||||
|
let n00 = sphere_dir(pole, u_dir, v_dir, theta0, phi0);
|
||||||
|
let n10 = sphere_dir(pole, u_dir, v_dir, theta0, phi1);
|
||||||
|
let n11 = sphere_dir(pole, u_dir, v_dir, theta1, phi1);
|
||||||
|
let n01 = sphere_dir(pole, u_dir, v_dir, theta1, phi0);
|
||||||
|
|
||||||
|
let p = [
|
||||||
|
[cx + r * n00[0], cy + r * n00[1], cz + r * n00[2]],
|
||||||
|
[cx + r * n10[0], cy + r * n10[1], cz + r * n10[2]],
|
||||||
|
[cx + r * n11[0], cy + r * n11[1], cz + r * n11[2]],
|
||||||
|
[cx + r * n01[0], cy + r * n01[1], cz + r * n01[2]],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Average outward normal for the quad.
|
||||||
|
let nav = norm3([
|
||||||
|
n00[0] + n10[0] + n11[0] + n01[0],
|
||||||
|
n00[1] + n10[1] + n11[1] + n01[1],
|
||||||
|
n00[2] + n10[2] + n11[2] + n01[2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
push_quad(verts, normals, indices, p, nav);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sphere_dir(
|
||||||
|
pole: [f64; 3],
|
||||||
|
u_dir: [f64; 3],
|
||||||
|
v_dir: [f64; 3],
|
||||||
|
theta: f64,
|
||||||
|
phi: f64,
|
||||||
|
) -> [f64; 3] {
|
||||||
|
let sin_phi = phi.sin();
|
||||||
|
let cos_phi = phi.cos();
|
||||||
|
let cos_theta = theta.cos();
|
||||||
|
let sin_theta = theta.sin();
|
||||||
|
// pole × cos_phi + (u*cos_theta + v*sin_theta) × sin_phi
|
||||||
|
[
|
||||||
|
pole[0] * cos_phi + (u_dir[0] * cos_theta + v_dir[0] * sin_theta) * sin_phi,
|
||||||
|
pole[1] * cos_phi + (u_dir[1] * cos_theta + v_dir[1] * sin_theta) * sin_phi,
|
||||||
|
pole[2] * cos_phi + (u_dir[2] * cos_theta + v_dir[2] * sin_theta) * sin_phi,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Torus face ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn tess_torus_face(
|
||||||
|
torus: &SatTorusSurface,
|
||||||
|
verts: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
) {
|
||||||
|
let (cx, cy, cz) = torus.center();
|
||||||
|
let (nx, ny, nz) = torus.normal();
|
||||||
|
let axis = norm3([nx, ny, nz]); // revolution axis
|
||||||
|
let (ux, uy, uz) = torus.u_direction();
|
||||||
|
let u_dir = norm3([ux, uy, uz]);
|
||||||
|
let v_dir = cross3(axis, u_dir);
|
||||||
|
let major_r = torus.major_radius();
|
||||||
|
let minor_r = torus.minor_radius();
|
||||||
|
|
||||||
|
let nu = GRID_U; // around the tube
|
||||||
|
let nv = GRID_V; // around the torus
|
||||||
|
|
||||||
|
for j in 0..nv {
|
||||||
|
let phi0 = TAU * (j as f64 / nv as f64);
|
||||||
|
let phi1 = TAU * ((j + 1) as f64 / nv as f64);
|
||||||
|
|
||||||
|
for i in 0..nu {
|
||||||
|
let theta0 = TAU * (i as f64 / nu as f64);
|
||||||
|
let theta1 = TAU * ((i + 1) as f64 / nu as f64);
|
||||||
|
|
||||||
|
let p = [
|
||||||
|
torus_pt(cx, cy, cz, axis, u_dir, v_dir, major_r, minor_r, theta0, phi0),
|
||||||
|
torus_pt(cx, cy, cz, axis, u_dir, v_dir, major_r, minor_r, theta0, phi1),
|
||||||
|
torus_pt(cx, cy, cz, axis, u_dir, v_dir, major_r, minor_r, theta1, phi1),
|
||||||
|
torus_pt(cx, cy, cz, axis, u_dir, v_dir, major_r, minor_r, theta1, phi0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Outward tube normal.
|
||||||
|
let mid_phi = (phi0 + phi1) * 0.5;
|
||||||
|
let mid_theta = (theta0 + theta1) * 0.5;
|
||||||
|
// Direction from tube center to surface point.
|
||||||
|
let radial = [
|
||||||
|
u_dir[0] * mid_phi.cos() + v_dir[0] * mid_phi.sin(),
|
||||||
|
u_dir[1] * mid_phi.cos() + v_dir[1] * mid_phi.sin(),
|
||||||
|
u_dir[2] * mid_phi.cos() + v_dir[2] * mid_phi.sin(),
|
||||||
|
];
|
||||||
|
let n = norm3([
|
||||||
|
radial[0] * mid_theta.cos() + axis[0] * mid_theta.sin(),
|
||||||
|
radial[1] * mid_theta.cos() + axis[1] * mid_theta.sin(),
|
||||||
|
radial[2] * mid_theta.cos() + axis[2] * mid_theta.sin(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
push_quad(verts, normals, indices, p, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn torus_pt(
|
||||||
|
cx: f64, cy: f64, cz: f64,
|
||||||
|
axis: [f64; 3],
|
||||||
|
u_dir: [f64; 3],
|
||||||
|
v_dir: [f64; 3],
|
||||||
|
major_r: f64,
|
||||||
|
minor_r: f64,
|
||||||
|
theta: f64, // tube angle
|
||||||
|
phi: f64, // revolution angle
|
||||||
|
) -> [f64; 3] {
|
||||||
|
// Ring center at angle phi.
|
||||||
|
let ring = [
|
||||||
|
cx + major_r * (u_dir[0] * phi.cos() + v_dir[0] * phi.sin()),
|
||||||
|
cy + major_r * (u_dir[1] * phi.cos() + v_dir[1] * phi.sin()),
|
||||||
|
cz + major_r * (u_dir[2] * phi.cos() + v_dir[2] * phi.sin()),
|
||||||
|
];
|
||||||
|
// Radial direction from torus axis to ring center.
|
||||||
|
let radial = norm3([ring[0] - cx, ring[1] - cy, ring[2] - cz]);
|
||||||
|
// Point on tube.
|
||||||
|
[
|
||||||
|
ring[0] + minor_r * (radial[0] * theta.cos() + axis[0] * theta.sin()),
|
||||||
|
ring[1] + minor_r * (radial[1] * theta.cos() + axis[1] * theta.sin()),
|
||||||
|
ring[2] + minor_r * (radial[2] * theta.cos() + axis[2] * theta.sin()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Math helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||||
|
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||||
|
[
|
||||||
|
a[1] * b[2] - a[2] * b[1],
|
||||||
|
a[2] * b[0] - a[0] * b[2],
|
||||||
|
a[0] * b[1] - a[1] * b[0],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn norm3(v: [f64; 3]) -> [f64; 3] {
|
||||||
|
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||||
|
if len < 1e-12 {
|
||||||
|
[0.0, 0.0, 1.0]
|
||||||
|
} else {
|
||||||
|
[v[0] / len, v[1] / len, v[2] / len]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue