feat(solid3d): draw ISOLINES on curved faces in wireframe

A cylinder's wireframe showed only its two rim circles, so from the side it
read as two disconnected ellipses rather than a cylinder. AutoCAD draws
ISOLINES — a fixed set of longitudinal lines on each curved face — so the
surface reads from any angle.

`collect_isolines` walks the cone-surface faces (cylinder and cone), recovers
each face's height and angular extent with the same logic `tess_cone_face`
uses, and emits `header.isolines` (default 4) longitudinal segments from the
bottom rim to the top rim. They are appended to the feature-edge line list, so
they ride the existing body transform, double-single split, and top-level /
per-INSTANCE re-split for free.

Being part of `edge_verts`, they inherit the edge pass's gating: drawn in the
wireframe modes and in shaded-with-edges, hidden in plain shaded — matching
where AutoCAD shows isolines. `isolines` is threaded from the header through
the tessellation entry points alongside `facet_res`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-17 19:45:42 +03:00
commit 0ce150411a
4 changed files with 115 additions and 22 deletions

View file

@ -71,12 +71,13 @@ impl Scene {
None
};
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
) {
let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines)
.map(|m| offset_mesh_lod_set(m))
} else {
None
@ -247,12 +248,13 @@ impl Scene {
None
};
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
) {
let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines)
.map(|m| offset_mesh_lod_set(m))
} else {
None
@ -327,6 +329,7 @@ impl Scene {
None
};
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
entity,
EntityType::Solid3D(_)
@ -335,7 +338,7 @@ impl Scene {
| EntityType::Surface(_)
) {
let color = self.render_style(entity).0;
crate::entities::solid3d::tessellate_volume(entity, color, facet_res)
crate::entities::solid3d::tessellate_volume(entity, color, facet_res, isolines)
.map(offset_mesh_lod_set)
} else {
None
@ -1705,13 +1708,14 @@ impl Scene {
use crate::par::prelude::*;
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
// Top-level solids: offset into the render frame, drawn flat.
// Block-definition solids: keep block-local coords for per-INSERT
// instancing (no offset applied here).
let built: Vec<(Handle, MeshLodSet, bool)> = entries
.into_par_iter()
.filter_map(|(handle, entity, color, top_level)| {
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res).map(|m| {
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res, isolines).map(|m| {
let m = if top_level { offset_mesh_lod_set(m) } else { m };
(handle, m, top_level)
})