forked from 1b-agent/opencadstudio-upstream
feat(scene): tessellate loaded ACIS solids via the truck B-rep kernel
Rebuild every ACIS face as a truck topological Face and mesh it with truck, routing loaded 3DSOLID/BODY/REGION/SURFACE through the same kernel the Model tab uses. plane→try_attach_plane, cone/cylinder/sphere/torus→revolution faces (truck's Surface has no cone/sphere/torus), spline→BSplineSurface grid sample. Per-vertex normals are computed analytically outward per surface type and triangle winding is oriented to match, since truck's own orientation isn't consistent across independently built faces. `tessellate_solid3d|body|region| surface` try this first and fall back to the bespoke sampler. Light the mesh shader with three spread directional lights plus ambient (abs(dot) per light, two-sided) so faces — including the inner walls of an open lofted surface — never read as a flat dark mass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
9d823363f1
commit
0badf48454
4 files changed
+482
-16
No files matched your search
@@ -0,0 +1,431 @@
|
||||
// ACIS B-rep → truck B-rep → mesh.
|
||||
//
|
||||
// Rebuilds every ACIS face as a truck topological `Face` and lets truck's
|
||||
// meshing kernel triangulate it, routing loaded 3DSOLID / BODY / REGION /
|
||||
// SURFACE geometry through the same NURBS/B-rep kernel the Model tab builds on
|
||||
// instead of the bespoke per-surface sampler in `solid3d_tess`.
|
||||
//
|
||||
// plane-surface → planar face from the sampled boundary loop
|
||||
// cone-surface → surface of revolution (cylinder / cone) via rsweep
|
||||
// sphere-surface → meridian revolved about the pole
|
||||
// torus-surface → tube circle revolved about the spine
|
||||
// spline-surface → truck BSplineSurface, grid-sampled (see spline_tess)
|
||||
//
|
||||
// Each face is meshed independently and its triangles are oriented outward
|
||||
// using an analytic per-surface normal — truck's own face orientation is not
|
||||
// consistent across independently built faces, so normals/winding are derived
|
||||
// from geometry instead. Faces whose surface type isn't handled are skipped;
|
||||
// the caller falls back to `solid3d_tess` when this returns `None`.
|
||||
|
||||
use truck_meshalgo::tessellation::{MeshableShape, MeshedShape};
|
||||
use truck_modeling::{builder, Face, InnerSpace, Point3, Rad, Shell, Vector3, Wire};
|
||||
use truck_polymesh::PolygonMesh;
|
||||
|
||||
use acadrust::entities::acis::types::Sense;
|
||||
use acadrust::entities::acis::{
|
||||
SatConeSurface, SatDocument, SatFace, SatPlaneSurface, SatSphereSurface, SatTorusSurface,
|
||||
};
|
||||
|
||||
use crate::scene::mesh_model::{MeshLodSet, MeshModel};
|
||||
use crate::scene::solid3d_tess::{
|
||||
apply_body_transform, body_transform, collect_face_polygon, cone_axis_span, mesh_aabb,
|
||||
};
|
||||
|
||||
/// Slightly over 2π so revolution builders close the loop.
|
||||
const FULL: f64 = std::f64::consts::TAU + 0.2;
|
||||
/// Boundary sampling density for planar faces with curved (circular) edges.
|
||||
const BOUNDARY_SEGS: usize = 64;
|
||||
/// Triangle mesh chord tolerance (world units).
|
||||
const MESH_TOL: f64 = 0.01;
|
||||
|
||||
/// Analytic outward-normal rule for a face, used to orient triangles and
|
||||
/// supply smooth per-vertex normals (truck's face orientation is unreliable
|
||||
/// for independently built faces).
|
||||
enum Outward {
|
||||
/// Constant normal (planar face).
|
||||
Const([f64; 3]),
|
||||
/// Cone/cylinder: radial away from the axis, tilted by the half-angle.
|
||||
Cone {
|
||||
center: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
sin: f64,
|
||||
cos: f64,
|
||||
},
|
||||
/// Sphere: radial from the centre.
|
||||
Sphere { center: [f64; 3] },
|
||||
/// Torus: from the nearest point on the spine circle.
|
||||
Torus {
|
||||
center: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
major: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl Outward {
|
||||
fn at(&self, p: [f64; 3]) -> [f64; 3] {
|
||||
match self {
|
||||
Outward::Const(n) => *n,
|
||||
Outward::Cone {
|
||||
center,
|
||||
axis,
|
||||
sin,
|
||||
cos,
|
||||
} => {
|
||||
let d = vsub(p, *center);
|
||||
let h = vdot(d, *axis);
|
||||
let radial = vnorm(vsub(d, vscale(*axis, h)));
|
||||
vnorm(vsub(vscale(radial, *cos), vscale(*axis, *sin)))
|
||||
}
|
||||
Outward::Sphere { center } => vnorm(vsub(p, *center)),
|
||||
Outward::Torus {
|
||||
center,
|
||||
axis,
|
||||
major,
|
||||
} => {
|
||||
let d = vsub(p, *center);
|
||||
let h = vdot(d, *axis);
|
||||
let radial = vsub(d, vscale(*axis, h));
|
||||
let rl = vlen(radial);
|
||||
let spine = if rl > 1e-9 {
|
||||
vadd(*center, vscale(radial, major / rl))
|
||||
} else {
|
||||
*center
|
||||
};
|
||||
vnorm(vsub(p, spine))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tessellate an ACIS SAT document by rebuilding it as truck faces.
|
||||
///
|
||||
/// Returns `None` when no face could be rebuilt (caller should fall back to
|
||||
/// the bespoke sampler).
|
||||
pub fn tessellate_sat_truck(
|
||||
sat: &SatDocument,
|
||||
name: String,
|
||||
color: [f32; 4],
|
||||
_facet_res: f64,
|
||||
) -> Option<MeshLodSet> {
|
||||
let mut mesh = MeshModel {
|
||||
name: name.clone(),
|
||||
verts: Vec::new(),
|
||||
normals: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
color,
|
||||
selected: false,
|
||||
};
|
||||
|
||||
for face in sat.faces() {
|
||||
let Some(surf_rec) = sat.resolve(face.surface()) else {
|
||||
continue;
|
||||
};
|
||||
let Some((faces, outward)) = build_face_group(sat, &face, surf_rec) else {
|
||||
continue;
|
||||
};
|
||||
if faces.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let shell: Shell = faces.into();
|
||||
let poly = shell.triangulation(MESH_TOL).to_polygon();
|
||||
append_group(&mut mesh, &poly, &outward);
|
||||
}
|
||||
|
||||
// Spline (NURBS) faces are meshed by direct grid sampling of the truck
|
||||
// BSplineSurface — see spline_tess — and merged into the same buffers.
|
||||
append_spline_faces(sat, &mut mesh);
|
||||
|
||||
if mesh.indices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((m, tr, scale)) = body_transform(sat) {
|
||||
apply_body_transform(&mut mesh, &m, &tr, scale);
|
||||
}
|
||||
let world_aabb = mesh_aabb(&mesh);
|
||||
Some(MeshLodSet {
|
||||
lods: vec![mesh],
|
||||
world_aabb,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the truck face(s) + outward rule for one analytic ACIS face.
|
||||
fn build_face_group(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
surf_rec: &acadrust::entities::acis::SatRecord,
|
||||
) -> Option<(Vec<Face>, Outward)> {
|
||||
match surf_rec.entity_type.as_str() {
|
||||
"plane-surface" => {
|
||||
let plane = SatPlaneSurface::from_record(surf_rec)?;
|
||||
let f = plane_face(sat, face)?;
|
||||
let (nx, ny, nz) = plane.normal();
|
||||
let n = if matches!(face.sense(), Sense::Reversed) {
|
||||
[-nx, -ny, -nz]
|
||||
} else {
|
||||
[nx, ny, nz]
|
||||
};
|
||||
Some((vec![f], Outward::Const(vnorm(n))))
|
||||
}
|
||||
"cone-surface" => {
|
||||
let cone = SatConeSurface::from_record(surf_rec)?;
|
||||
let (faces, out) = cone_faces(sat, &cone)?;
|
||||
Some((faces, out))
|
||||
}
|
||||
"sphere-surface" => {
|
||||
let sphere = SatSphereSurface::from_record(surf_rec)?;
|
||||
let (cx, cy, cz) = sphere.center();
|
||||
Some((sphere_faces(&sphere), Outward::Sphere { center: [cx, cy, cz] }))
|
||||
}
|
||||
"torus-surface" => {
|
||||
let torus = SatTorusSurface::from_record(surf_rec)?;
|
||||
let (cx, cy, cz) = torus.center();
|
||||
let (nx, ny, nz) = torus.normal();
|
||||
let out = Outward::Torus {
|
||||
center: [cx, cy, cz],
|
||||
axis: vnorm([nx, ny, nz]),
|
||||
major: torus.major_radius(),
|
||||
};
|
||||
Some((torus_faces(&torus), out))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Planar face ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a planar truck face from a face's sampled boundary loop. Curved
|
||||
/// boundary edges (circles) are sampled into line segments, which keeps the
|
||||
/// wire planar so `try_attach_plane` can fit the plane.
|
||||
fn plane_face(sat: &SatDocument, face: &SatFace) -> Option<Face> {
|
||||
let poly = collect_face_polygon(sat, face, BOUNDARY_SEGS);
|
||||
if poly.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let verts: Vec<_> = poly
|
||||
.iter()
|
||||
.map(|p| builder::vertex(Point3::new(p[0], p[1], p[2])))
|
||||
.collect();
|
||||
let n = verts.len();
|
||||
let edges: Vec<_> = (0..n)
|
||||
.map(|i| builder::line(&verts[i], &verts[(i + 1) % n]))
|
||||
.collect();
|
||||
let wire: Wire = edges.into();
|
||||
builder::try_attach_plane(&[wire]).ok()
|
||||
}
|
||||
|
||||
// ── Cone / cylinder face ─────────────────────────────────────────────────────
|
||||
|
||||
/// Build the lateral surface of a cone/cylinder as revolution faces. The
|
||||
/// height span comes from the solid's coaxial rims (plus a true cone's apex).
|
||||
fn cone_faces(sat: &SatDocument, cone: &SatConeSurface) -> Option<(Vec<Face>, Outward)> {
|
||||
let (cx, cy, cz) = cone.center();
|
||||
let (ax, ay, az) = cone.axis();
|
||||
let (ux, uy, uz) = cone.major_axis();
|
||||
let radius = cone.radius();
|
||||
let sin = cone.sin_half_angle();
|
||||
let cos = cone.cos_half_angle();
|
||||
|
||||
let axis = norm(Vector3::new(ax, ay, az));
|
||||
let udir = norm(Vector3::new(ux, uy, uz));
|
||||
let center = Point3::new(cx, cy, cz);
|
||||
|
||||
let (hmin, hmax) = cone_axis_span(sat, cone, [axis.x, axis.y, axis.z], [cx, cy, cz])?;
|
||||
let r_at = |h: f64| {
|
||||
if cos.abs() > 1e-9 {
|
||||
radius + h * sin / cos
|
||||
} else {
|
||||
radius
|
||||
}
|
||||
};
|
||||
|
||||
let p0 = center + udir * r_at(hmin) + axis * hmin;
|
||||
let p1 = center + udir * r_at(hmax) + axis * hmax;
|
||||
let profile = builder::line(&builder::vertex(p0), &builder::vertex(p1));
|
||||
let shell: Shell = builder::rsweep(&profile, center, axis, Rad(FULL));
|
||||
|
||||
let out = Outward::Cone {
|
||||
center: [cx, cy, cz],
|
||||
axis: [axis.x, axis.y, axis.z],
|
||||
sin,
|
||||
cos,
|
||||
};
|
||||
Some((shell.face_iter().cloned().collect(), out))
|
||||
}
|
||||
|
||||
// ── Sphere face ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn sphere_faces(sphere: &SatSphereSurface) -> Vec<Face> {
|
||||
let (cx, cy, cz) = sphere.center();
|
||||
let r = sphere.radius();
|
||||
let (px, py, pz) = sphere.pole();
|
||||
let (ux, uy, uz) = sphere.u_direction();
|
||||
|
||||
let center = Point3::new(cx, cy, cz);
|
||||
let pole = norm(Vector3::new(px, py, pz));
|
||||
let mut perp = Vector3::new(ux, uy, uz);
|
||||
if perp.magnitude2() < 1e-12 || perp.dot(pole).abs() > 0.999 {
|
||||
perp = perpendicular(pole);
|
||||
} else {
|
||||
perp = norm(perp - pole * perp.dot(pole));
|
||||
}
|
||||
|
||||
let top = builder::vertex(center + pole * r);
|
||||
let meridian: Wire = builder::rsweep(&top, center, perp, Rad(std::f64::consts::PI));
|
||||
let shell: Shell = builder::cone(&meridian, pole, Rad(FULL));
|
||||
shell.face_iter().cloned().collect()
|
||||
}
|
||||
|
||||
// ── Torus face ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn torus_faces(torus: &SatTorusSurface) -> Vec<Face> {
|
||||
let (cx, cy, cz) = torus.center();
|
||||
let (nx, ny, nz) = torus.normal();
|
||||
let (ux, uy, uz) = torus.u_direction();
|
||||
let major = torus.major_radius();
|
||||
let minor = torus.minor_radius();
|
||||
|
||||
let center = Point3::new(cx, cy, cz);
|
||||
let axis = norm(Vector3::new(nx, ny, nz));
|
||||
let udir = norm(Vector3::new(ux, uy, uz));
|
||||
let binormal = norm(axis.cross(udir));
|
||||
|
||||
let ring_center = center + udir * major;
|
||||
let tube_start = builder::vertex(ring_center + axis * minor);
|
||||
let tube: Wire = builder::rsweep(&tube_start, ring_center, binormal, Rad(FULL));
|
||||
let shell: Shell = builder::rsweep(&tube, center, axis, Rad(FULL));
|
||||
shell.face_iter().cloned().collect()
|
||||
}
|
||||
|
||||
// ── Spline faces (NURBS) ─────────────────────────────────────────────────────
|
||||
|
||||
/// Append meshes for every `spline-surface` face, reusing the truck
|
||||
/// BSplineSurface grid sampler in `spline_tess`.
|
||||
fn append_spline_faces(sat: &SatDocument, mesh: &mut MeshModel) {
|
||||
use crate::scene::solid3d_tess::LodConfig;
|
||||
for face in sat.faces() {
|
||||
let Some(surf_rec) = sat.resolve(face.surface()) else {
|
||||
continue;
|
||||
};
|
||||
if surf_rec.entity_type != "spline-surface" {
|
||||
continue;
|
||||
}
|
||||
let mut verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut normals: Vec<[f32; 3]> = Vec::new();
|
||||
let mut indices: Vec<u32> = Vec::new();
|
||||
crate::scene::spline_tess::tess_spline_face(
|
||||
sat,
|
||||
&face,
|
||||
LodConfig::HIGH,
|
||||
&mut verts,
|
||||
&mut normals,
|
||||
&mut indices,
|
||||
);
|
||||
let base = mesh.verts.len() as u32;
|
||||
mesh.verts.extend_from_slice(&verts);
|
||||
mesh.normals.extend_from_slice(&normals);
|
||||
mesh.indices.extend(indices.iter().map(|i| i + base));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mesh append with analytic outward normals ────────────────────────────────
|
||||
|
||||
/// Append one face's triangulation to `mesh`, computing smooth per-vertex
|
||||
/// normals from the outward rule and flipping any triangle whose winding
|
||||
/// disagrees with that outward direction.
|
||||
fn append_group(mesh: &mut MeshModel, poly: &PolygonMesh, outward: &Outward) {
|
||||
let positions = poly.positions();
|
||||
let base = mesh.verts.len() as u32;
|
||||
for p in positions {
|
||||
let pos = [p.x, p.y, p.z];
|
||||
mesh.verts.push([p.x as f32, p.y as f32, p.z as f32]);
|
||||
let n = outward.at(pos);
|
||||
mesh.normals.push([n[0] as f32, n[1] as f32, n[2] as f32]);
|
||||
}
|
||||
for tri in poly.tri_faces() {
|
||||
let (i0, i1, i2) = (tri[0].pos, tri[1].pos, tri[2].pos);
|
||||
let a = pt(positions[i0]);
|
||||
let b = pt(positions[i1]);
|
||||
let c = pt(positions[i2]);
|
||||
let gn = vcross(vsub(b, a), vsub(c, a));
|
||||
let cen = [
|
||||
(a[0] + b[0] + c[0]) / 3.0,
|
||||
(a[1] + b[1] + c[1]) / 3.0,
|
||||
(a[2] + b[2] + c[2]) / 3.0,
|
||||
];
|
||||
let out = outward.at(cen);
|
||||
let (j0, j1, j2) = (base + i0 as u32, base + i1 as u32, base + i2 as u32);
|
||||
if vdot(gn, out) < 0.0 {
|
||||
mesh.indices.extend_from_slice(&[j0, j2, j1]);
|
||||
} else {
|
||||
mesh.indices.extend_from_slice(&[j0, j1, j2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn pt(p: Point3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
// ── Vector helpers (cgmath Vector3) ──────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn norm(v: Vector3) -> Vector3 {
|
||||
let len = v.magnitude();
|
||||
if len < 1e-12 {
|
||||
Vector3::unit_z()
|
||||
} else {
|
||||
v / len
|
||||
}
|
||||
}
|
||||
|
||||
/// Any unit vector perpendicular to `v`.
|
||||
fn perpendicular(v: Vector3) -> Vector3 {
|
||||
let a = if v.x.abs() < 0.9 {
|
||||
Vector3::unit_x()
|
||||
} else {
|
||||
Vector3::unit_y()
|
||||
};
|
||||
norm(v.cross(a))
|
||||
}
|
||||
|
||||
// ── Vector helpers ([f64; 3]) ────────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn vsub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
#[inline]
|
||||
fn vadd(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||||
}
|
||||
#[inline]
|
||||
fn vscale(a: [f64; 3], s: f64) -> [f64; 3] {
|
||||
[a[0] * s, a[1] * s, a[2] * s]
|
||||
}
|
||||
#[inline]
|
||||
fn vdot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
#[inline]
|
||||
fn vcross(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 vlen(a: [f64; 3]) -> f64 {
|
||||
vdot(a, a).sqrt()
|
||||
}
|
||||
#[inline]
|
||||
fn vnorm(a: [f64; 3]) -> [f64; 3] {
|
||||
let l = vlen(a);
|
||||
if l < 1e-12 {
|
||||
[0.0, 0.0, 1.0]
|
||||
} else {
|
||||
[a[0] / l, a[1] / l, a[2] / l]
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ pub(super) struct EntityIndex {
|
||||
}
|
||||
pub(crate) mod render;
|
||||
mod selection;
|
||||
pub mod acis_to_truck;
|
||||
pub mod solid3d_tess;
|
||||
pub mod spline_tess;
|
||||
pub mod tess_util;
|
||||
|
||||
+36
-11
@@ -132,6 +132,31 @@ fn tessellate_sat(
|
||||
})
|
||||
}
|
||||
|
||||
/// Tessellate an ACIS document, preferring the truck B-rep kernel and falling
|
||||
/// back to the bespoke per-surface sampler when truck can't rebuild the shell
|
||||
/// (e.g. an unhandled surface type).
|
||||
fn tessellate_acis(
|
||||
sat: &SatDocument,
|
||||
name: String,
|
||||
color: [f32; 4],
|
||||
facet_res: f64,
|
||||
) -> Option<MeshLodSet> {
|
||||
if let Some(set) = crate::scene::acis_to_truck::tessellate_sat_truck(sat, name.clone(), color, facet_res) {
|
||||
if std::env::var_os("OCS_TESS_DEBUG").is_some() {
|
||||
let tris = set.lods.first().map(|m| m.indices.len() / 3).unwrap_or(0);
|
||||
eprintln!("acis_tess[{name}]: truck ({tris} tris)");
|
||||
}
|
||||
return Some(set);
|
||||
}
|
||||
// truck couldn't rebuild the shell — fall back to the bespoke sampler.
|
||||
let fallback = tessellate_sat_lods(sat, name.clone(), color, facet_res);
|
||||
eprintln!(
|
||||
"acis_tess[{name}]: manual fallback ({})",
|
||||
if fallback.is_some() { "ok" } else { "empty" }
|
||||
);
|
||||
fallback
|
||||
}
|
||||
|
||||
/// Tessellate a SAT document at all three LODs and bundle them into a
|
||||
/// `MeshLodSet` ready for the render pipeline to pick a level per frame.
|
||||
fn tessellate_sat_lods(
|
||||
@@ -164,7 +189,7 @@ fn tessellate_sat_lods(
|
||||
/// geometry in body-local space and records the placement in a `transform`
|
||||
/// record (`<3×3> <tx ty tz> <scale> rotate reflect shear`). `None` when the
|
||||
/// document has no transform (treated as identity).
|
||||
fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f64)> {
|
||||
pub(crate) fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f64)> {
|
||||
let t = sat.records.iter().find(|r| r.entity_type == "transform")?;
|
||||
let mut m = [0.0f64; 9];
|
||||
for (i, slot) in m.iter_mut().enumerate() {
|
||||
@@ -178,7 +203,7 @@ fn body_transform(sat: &SatDocument) -> Option<([f64; 9], [f64; 3], f64)> {
|
||||
/// Apply a body placement transform to a mesh. ACIS treats points as row
|
||||
/// vectors (`p' = scale·(p·M) + T`), so the 3×3 is indexed transposed relative
|
||||
/// to a column-vector multiply. Normals get the rotation only, renormalized.
|
||||
fn apply_body_transform(mesh: &mut MeshModel, m: &[f64; 9], tr: &[f64; 3], scale: f64) {
|
||||
pub(crate) fn apply_body_transform(mesh: &mut MeshModel, m: &[f64; 9], tr: &[f64; 3], scale: f64) {
|
||||
for v in &mut mesh.verts {
|
||||
let (x, y, z) = (v[0] as f64, v[1] as f64, v[2] as f64);
|
||||
let wx = scale * (x * m[0] + y * m[3] + z * m[6]) + tr[0];
|
||||
@@ -212,7 +237,7 @@ fn scale_lod(base: LodConfig, facet_res: f64) -> LodConfig {
|
||||
|
||||
/// World-XY AABB of the mesh — used by the render-pipeline LOD selector
|
||||
/// to pick a level based on projected pixel diagonal.
|
||||
fn mesh_aabb(mesh: &MeshModel) -> [f32; 4] {
|
||||
pub(crate) fn mesh_aabb(mesh: &MeshModel) -> [f32; 4] {
|
||||
let mut min_x = f32::INFINITY;
|
||||
let mut min_y = f32::INFINITY;
|
||||
let mut max_x = f32::NEG_INFINITY;
|
||||
@@ -251,7 +276,7 @@ pub fn tessellate_region(region: &Region, color: [f32; 4], facet_res: f64) -> Op
|
||||
®ion.acis_data.sab_data,
|
||||
)?;
|
||||
let name = region.common.handle.value().to_string();
|
||||
tessellate_sat_lods(&sat, name, color, facet_res)
|
||||
tessellate_acis(&sat, name, color, facet_res)
|
||||
}
|
||||
|
||||
/// Tessellate a `Body` entity (3D ACIS body) at all three LOD levels.
|
||||
@@ -262,7 +287,7 @@ pub fn tessellate_body(body: &Body, color: [f32; 4], facet_res: f64) -> Option<M
|
||||
&body.acis_data.sab_data,
|
||||
)?;
|
||||
let name = body.common.handle.value().to_string();
|
||||
tessellate_sat_lods(&sat, name, color, facet_res)
|
||||
tessellate_acis(&sat, name, color, facet_res)
|
||||
}
|
||||
|
||||
/// Tessellate a `Surface` entity (ACAD_SURFACE family) at all three LOD
|
||||
@@ -280,7 +305,7 @@ pub fn tessellate_surface(
|
||||
&surface.acis_data.sab_data,
|
||||
)?;
|
||||
let name = surface.common.handle.value().to_string();
|
||||
tessellate_sat_lods(&sat, name, color, facet_res)
|
||||
tessellate_acis(&sat, name, color, facet_res)
|
||||
}
|
||||
|
||||
/// Tessellate a `Solid3D` entity at all three LOD levels.
|
||||
@@ -295,7 +320,7 @@ pub fn tessellate_solid3d(solid: &Solid3D, color: [f32; 4], facet_res: f64) -> O
|
||||
&solid.acis_data.sab_data,
|
||||
)?;
|
||||
let name = solid.common.handle.value().to_string();
|
||||
tessellate_sat_lods(&sat, name, color, facet_res)
|
||||
tessellate_acis(&sat, name, color, facet_res)
|
||||
}
|
||||
|
||||
// ── Topology helpers ──────────────────────────────────────────────────────────
|
||||
@@ -310,7 +335,7 @@ pub fn tessellate_solid3d(solid: &Solid3D, color: [f32; 4], facet_res: f64) -> O
|
||||
///
|
||||
/// 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, circ_segs: usize) -> Vec<[f64; 3]> {
|
||||
pub(crate) fn collect_face_polygon(sat: &SatDocument, face: &SatFace, circ_segs: usize) -> Vec<[f64; 3]> {
|
||||
let loop_ptr = face.first_loop();
|
||||
let Some(loop_rec) = sat.resolve(loop_ptr) else {
|
||||
return vec![];
|
||||
@@ -355,7 +380,7 @@ fn collect_face_polygon(sat: &SatDocument, face: &SatFace, circ_segs: usize) ->
|
||||
/// sampled along their parametric arc (excluding the end param so the next
|
||||
/// coedge's start point provides the junction); all other curve types fall
|
||||
/// back to the single start vertex, respecting coedge sense.
|
||||
fn append_coedge_points(
|
||||
pub(crate) fn append_coedge_points(
|
||||
sat: &SatDocument,
|
||||
coedge: &SatCoedge,
|
||||
circ_segs: usize,
|
||||
@@ -442,7 +467,7 @@ fn sample_ellipse_arc(
|
||||
}
|
||||
|
||||
/// Resolve a vertex pointer all the way to its `[x, y, z]` coordinate.
|
||||
fn resolve_point(sat: &SatDocument, v_ptr: SatPointer) -> Option<[f64; 3]> {
|
||||
pub(crate) 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())?;
|
||||
@@ -702,7 +727,7 @@ fn angular_range(
|
||||
/// projects their centres onto the axis to get rim heights. For a true cone
|
||||
/// with a single rim, the tip is added analytically (the height where the
|
||||
/// radius reaches zero). Returns `None` when no coaxial rim is found.
|
||||
fn cone_axis_span(
|
||||
pub(crate) fn cone_axis_span(
|
||||
sat: &SatDocument,
|
||||
cone: &SatConeSurface,
|
||||
axis: [f64; 3],
|
||||
|
||||
+14
-5
@@ -49,8 +49,6 @@ fn vs_main(v: VertexIn) -> VertexOut {
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
// Fixed directional light (world space, tilted toward the viewer).
|
||||
let light_dir = normalize(vec3<f32>(0.4, 0.8, 0.6));
|
||||
var n: vec3<f32>;
|
||||
if (u.flat_shade > 0.5) {
|
||||
// Per-triangle face normal: derivatives of the interpolated
|
||||
@@ -60,8 +58,19 @@ fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||
} else {
|
||||
n = normalize(in.normal);
|
||||
}
|
||||
// Half-Lambert: [0.5, 1.0] so the dark side isn't fully black.
|
||||
let diff = clamp(dot(n, light_dir), 0.0, 1.0) * 0.5 + 0.5;
|
||||
let rgb = in.color.rgb * diff;
|
||||
|
||||
// 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.
|
||||
// `abs(dot)` makes each light two-sided (independent of normal direction).
|
||||
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))
|
||||
+ 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);
|
||||
}
|
||||
Reference in new issue
Block a user