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

+40 -2
View File
@@ -110,10 +110,25 @@ fn write_surface_property_state(surface: &mut Surface, state: SurfacePropertySta
surface.common.extended_data.upsert_record(record);
}
/// Upper bound for the header `$ISOLINES` value handed to tessellation —
/// the Options slider's own ceiling ("Isolines per surface", 0..=64).
pub(crate) const MAX_HEADER_ISOLINES: i16 = 64;
/// Upper bound for per-surface `u/v_isolines` — the plugin editor's
/// writable range for those fields (0..=200).
pub(crate) const MAX_SURFACE_ISOLINES: i16 = 200;
/// Header `$ISOLINES` (file- or command-controlled `i16`) → the bounded
/// per-face isolate count handed to the kernel, which draws `0..count`
/// isolines per face with no ceiling of its own.
pub(crate) fn clamp_header_isolines(raw: i16) -> usize {
raw.clamp(0, MAX_HEADER_ISOLINES) as usize
}
pub(crate) fn surface_isoline_counts(surface: &Surface) -> [usize; 2] {
[
surface.u_isolines.max(0) as usize,
surface.v_isolines.max(0) as usize,
surface.u_isolines.clamp(0, MAX_SURFACE_ISOLINES) as usize,
surface.v_isolines.clamp(0, MAX_SURFACE_ISOLINES) as usize,
]
}
@@ -939,4 +954,27 @@ mod tests {
assert_eq!(min, [0.0, 0.0, 0.0]);
assert_eq!(max, [10.0, 10.0, 0.0]);
}
/// `Surface.u/v_isolines` are raw `i16` — a file can carry 32767 and the
/// native property editor only floors at 0 — and the counts go straight
/// into the kernel, which draws `0..count` isolines per face. The
/// bounded answer the rest of the product agrees on is the plugin
/// editor's writable range, 0..=200.
#[test]
fn surface_isolate_counts_stay_within_the_plugin_editor_range() {
use codec::entities::{Surface, SurfaceKind};
let mut surface = Surface::new(SurfaceKind::Extruded);
surface.u_isolines = i16::MAX;
surface.v_isolines = -1;
assert_eq!(
surface_isoline_counts(&surface),
[200, 0],
"a hostile surface must not hand the kernel 32767 isolines"
);
surface.u_isolines = 8;
surface.v_isolines = 6;
assert_eq!(surface_isoline_counts(&surface), [8, 6]);
}
}