fix(tess): clamp isoline counts before they reach the kernel

Bug:
$ISOLINES (header i16) and Surface.u/v_isolines (file-controlled i16)
were only ever floored at 0, never capped, and were handed to the
tessellation kernel as the per-face isolate count - which draws
0..count isolines per face with no ceiling of its own. SETVAR ISOLINES
32767 (or a DWG carrying that header, or a Surface with u_isolines =
32767) turned every rebuild and every per-cursor preview of
PRESSPULL/REVOLVE/SWEEP/LOFT into a multi-million-vertex computation:
measured 4,259,966 edge vertices for one 12-unit cylinder preview.

Reproduce:
1. SETVAR ISOLINES 32767 (or open a DWG whose $ISOLINES is large, or
   edit a Surface whose u_isolines is 32767).
2. Draw a circle and start REVOLVE (or PRESSPULL/SWEEP/LOFT), or shade
   any 3D solid.
3. Before: the app allocates without bound - abort on memory failure or
   a permanent freeze; previews recompute on every mouse move.
4. After: previews stay interactive; a hostile header renders exactly
   like the largest value the Options slider can request.

Fix:
Cap each value at the ceiling this product already agrees on, where the
type changes from i16 to the kernel count: header $ISOLINES <= 64 (the
Options slider's own "Isolines per surface" range), Surface
u/v_isolines <= 200 (the plugin editor's writable range). One helper
(clamp_header_isolines) replaces all 11 copies of .max(0) as usize
across the entity/scene/control/display feed sites,
surface_isoline_counts clamps both axes, and SETVAR ISOLINES stores
x.clamp(0, 64) so the echo, slider and re-saved files agree with what
renders; file-sourced headers keep their raw stored value and are
clamped at consumption. Clamping is density-only: geometry, bounds and
saves are untouched. Three regression tests cover hostile surface
counts, the SETVAR entry, and a file-sourced header measured through
display tessellation (hostile renders like the slider ceiling, negative
renders like zero).
This commit is contained in:
Karim Jerbi committed 2026-09-30 19:15:49 +01:00
1 parent 438a46b80f
commit e15d801011
7 files changed
+115 -13

No files matched your search

+4 -4
View File
@@ -319,7 +319,7 @@ impl Scene {
let facet_res = self.document.header.facet_resolution;
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_res);
let isolines = self.document.header.isolines.max(0) as usize;
let isolines = crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines);
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_)
@@ -603,7 +603,7 @@ impl Scene {
let facet_res = self.document.header.facet_resolution;
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_res);
let isolines = self.document.header.isolines.max(0) as usize;
let isolines = crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines);
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_)
@@ -832,7 +832,7 @@ impl Scene {
let facet_res = self.document.header.facet_resolution;
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_res);
let isolines = self.document.header.isolines.max(0) as usize;
let isolines = crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines);
use crate::par::prelude::*;
let built: Vec<(Handle, MeshLodSet, bool)> = entries
.into_par_iter()
@@ -2578,7 +2578,7 @@ impl Scene {
let facet_res = self.document.header.facet_resolution;
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_res);
let isolines = self.document.header.isolines.max(0) as usize;
let isolines = crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines);
// 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).
+2 -2
View File
@@ -1294,7 +1294,7 @@ fn build_derived_caches_impl(
// definition solids keep block-local coords for per-INSERT instancing. (#123)
let facet_res = doc.header.facet_resolution;
let chordal_deflection = crate::entities::solid3d::display_deflection(&doc.header, facet_res);
let isolines = doc.header.isolines.max(0) as usize;
let isolines = crate::entities::solid3d::clamp_header_isolines(doc.header.isolines);
// Real layout blocks come from the Layout objects' block_record handles —
// `BlockRecord::is_layout()` is unreliable here (it flags ordinary blocks).
let layout_blocks: std::collections::HashSet<Handle> = doc
@@ -3818,7 +3818,7 @@ impl Scene {
crate::entities::solid3d::surface_isoline_counts(surface),
crate::entities::solid3d::surface_property_state(surface).isolines,
),
_ => ([self.document.header.isolines.max(0) as usize; 2], false),
_ => ([crate::entities::solid3d::clamp_header_isolines(self.document.header.isolines); 2], false),
};
let (mut set, wires, center) = crate::scene::model::solid_model::display_from_solid(
solid,
+35
View File
@@ -640,6 +640,41 @@ mod tests {
assert_eq!(display([2, 2], false), boundaries);
}
/// A file-sourced header `$ISOLINES` bypasses `SETVAR` entirely, so the
/// consumption side must clamp too: `prepare_solid_model_display` hands
/// the header count to the kernel for every curved face, and 32767
/// isolines on a cylinder is a ~100k-vertex preview rebuilt per cursor
/// event.
#[test]
fn header_isolines_are_clamped_before_display_tessellation() {
let measure = |value: i16| {
let mut scene = crate::scene::Scene::new();
scene.document.header.isolines = value;
let body = cylinder_solid([0.0; 3], 5.0, 12.0).unwrap();
scene
.prepare_solid_model_display(codec::Handle::new(0xDEAD), &body)
.expect("cylinder display")
.0
.edge_verts
.len()
};
// A hostile file-sourced header must render exactly like the
// largest value the Options slider can request — and a negative
// one exactly like zero.
let hostile = measure(i16::MAX);
assert_eq!(
hostile,
measure(crate::entities::solid3d::MAX_HEADER_ISOLINES),
"header.isolines must be clamped before tessellation"
);
assert_eq!(measure(-5), measure(0));
assert!(
hostile < 20_000,
"even the ceiling must stay a bounded preview: {hostile} vertices"
);
}
#[test]
fn box_face_centers_are_six() {
let body = box_solid([0.0, 0.0, 0.0], 10.0, 10.0, 10.0).unwrap();