forked from 1b-agent/opencadstudio-upstream
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:
1 parent
438a46b80f
commit
e15d801011
7 files changed
+115
-13
No files matched your search
@@ -782,7 +782,7 @@ impl OpenCADStudio {
|
||||
use crate::modules::insert::solid3d_cmds::PresspullCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let mut command = PresspullCommand::new(color);
|
||||
command.set_isolines(self.tabs[i].scene.document.header.isolines.max(0) as usize);
|
||||
command.set_isolines(crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines));
|
||||
command.set_preselection(self.presspull_preselection());
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
@@ -793,7 +793,7 @@ impl OpenCADStudio {
|
||||
use crate::modules::insert::solid3d_cmds::RevolveCommand;
|
||||
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize;
|
||||
let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines);
|
||||
let mut cmd = RevolveCommand::new(color, isolines);
|
||||
if !selected.is_empty() {
|
||||
cmd.set_preselection(
|
||||
@@ -811,7 +811,7 @@ impl OpenCADStudio {
|
||||
"SWEEP" => {
|
||||
use crate::modules::insert::solid3d_cmds::SweepCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize;
|
||||
let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines);
|
||||
let mut cmd = SweepCommand::new(color, isolines);
|
||||
let selected = self.tabs[i].scene.selected_handles_in_order()
|
||||
.into_iter()
|
||||
@@ -829,7 +829,7 @@ impl OpenCADStudio {
|
||||
"LOFT" => {
|
||||
use crate::modules::insert::solid3d_cmds::LoftCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize;
|
||||
let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines);
|
||||
let selected = self.tabs[i].scene.selected_handles_in_order().into_iter()
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
|
||||
.cloned().map(|entity| (handle, entity))).collect();
|
||||
|
||||
@@ -2223,6 +2223,14 @@ impl OpenCADStudio {
|
||||
Some(v) => v
|
||||
.parse::<i16>()
|
||||
.map(|x| {
|
||||
// Stored values stay inside the range
|
||||
// the Options slider can ask for; the
|
||||
// consumption sites clamp again for
|
||||
// file-sourced headers.
|
||||
let x = x.clamp(
|
||||
0,
|
||||
crate::entities::solid3d::MAX_HEADER_ISOLINES,
|
||||
);
|
||||
h.isolines = x;
|
||||
(format!("ISOLINES = {x}"), true)
|
||||
})
|
||||
@@ -3416,6 +3424,27 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
/// `SETVAR ISOLINES` stored any parseable `i16` verbatim. The header
|
||||
/// value is the per-face isolate count handed to the tessellation
|
||||
/// kernel (which loops `0..count`), so `ISOLINES 32767` made every
|
||||
/// rebuild and per-cursor preview allocate without bound. The stored
|
||||
/// value must stay inside the range the app's own UI can ask for
|
||||
/// (the Options slider is 0..=64).
|
||||
#[test]
|
||||
fn setvar_isolines_stays_within_the_renderable_range() {
|
||||
let mut app = fresh_app();
|
||||
let i = app.active_tab;
|
||||
|
||||
let _ = app.run_command_line("SETVAR ISOLINES 32767");
|
||||
assert_eq!(app.tabs[i].scene.document.header.isolines, 64);
|
||||
|
||||
let _ = app.run_command_line("SETVAR ISOLINES 8");
|
||||
assert_eq!(app.tabs[i].scene.document.header.isolines, 8);
|
||||
|
||||
let _ = app.run_command_line("SETVAR ISOLINES -3");
|
||||
assert_eq!(app.tabs[i].scene.document.header.isolines, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_commands_gather_instead_of_using_the_whole_drawing() {
|
||||
for command in ["SETBYLAYER", "OVERKILL"] {
|
||||
|
||||
@@ -1240,7 +1240,7 @@ impl OpenCADStudio {
|
||||
[1.; 4],
|
||||
h.facet_resolution,
|
||||
crate::entities::solid3d::display_deflection(h, h.facet_resolution),
|
||||
h.isolines.max(0) as usize,
|
||||
crate::entities::solid3d::clamp_header_isolines(h.isolines),
|
||||
)
|
||||
});
|
||||
let curve = crate::entities::curve::entity_curve(entity).map(|planar| {
|
||||
|
||||
+40
-2
@@ -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]);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user