feat(solid3d): view-dependent silhouettes (DISPSILH) on curved faces

ISOLINES show a curved solid from any angle, but its outline still cut across
whichever fixed longitudinal lines happened to face the eye. DISPSILH draws the
true silhouette — the two lines where a cylinder (or cone) turns edge-on to the
view — and it moves with the camera, so it cannot be a baked edge.

Tessellation stores a world-space generator per cone/cylinder face on the mesh
set (axis, radial frame, radius, cone taper, extent), sharing the face-extent
extraction with the ISOLINES collector. Each frame `upload_silhouettes` walks
those generators, solves `du·cosθ + dv·sinθ = -tanα·da` for the two edge-on
angles (`φ ± acos(...)`, `φ ± π/2` for a cylinder), and builds a line list in
the mesh vertex format — double-single, so it stays precise at UTM. It draws
through the existing wireframe / edge pipelines in the wireframe and
shaded-with-edges passes.

Gated on the document's DISPSILH header flag, which defaults off — so the
per-frame pass is a no-op unless the drawing asks for it. Scoped to top-level
solids: a block instance's generators aren't transformed by the INSTANCE pose
yet, so they are cleared on the block path (its isolines and feature edges
still draw).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-17 20:02:56 +03:00
commit ae5231427b
5 changed files with 329 additions and 51 deletions

View file

@ -87,6 +87,9 @@ pub struct ViewportData {
/// rendered on top of fills. Most shaded modes turn this off; the
/// `*WithEdges` variants and the pure wireframes leave it on.
pub(in crate::scene) show_3d_edges: bool,
/// DISPSILH — draw view-dependent silhouette outlines on curved solid faces.
/// A document-header global (default off), orthogonal to the visual style.
pub(in crate::scene) display_silhouette: bool,
/// HiddenLine routes 3D fills through a depth-only prepass so edges
/// occluded by closer geometry are culled by the LessEqual depth
/// test on the wire passes that follow.
@ -555,6 +558,14 @@ impl shader::Primitive for Primitive {
vp.uniforms.eye_high[1] as f64 + vp.uniforms.eye_low[1] as f64,
vp.uniforms.eye_high[2] as f64 + vp.uniforms.eye_low[2] as f64,
);
// DISPSILH: rebuild view-dependent silhouettes each frame (off by
// default, so this is a no-op unless the drawing enables it). Only
// in modes that draw edges — pure shaded hides them.
if vp.display_silhouette && (vp.view_wireframe || vp.show_3d_edges) {
inner.upload_silhouettes(device, &vp.meshes[..], eye);
} else {
inner.upload_silhouettes(device, &[], eye);
}
inner.compute_wire_scissors(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_wipeout_scissors(view_rot, eye, clip_size.width, clip_size.height);
inner.compute_image_scissors(view_rot, eye, clip_size.width, clip_size.height);
@ -1362,6 +1373,7 @@ impl Scene {
view_wireframe,
mesh_fill: flags.mesh_fill,
show_3d_edges: flags.show_3d_edges,
display_silhouette: self.document.header.display_silhouette,
hidden_line: flags.hidden_line,
// Interaction LOD: suppress the costly hatch pass while the view is
// actively moving; the scene-render cache holds the full-quality